@weckr/sdk 0.1.0 → 0.1.2

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/dist/index.d.mts CHANGED
@@ -3,6 +3,13 @@ interface WeckrConfig {
3
3
  apiKey: string;
4
4
  plans?: Record<string, number>;
5
5
  endpoint?: string;
6
+ /** Optional override for the cap-check endpoint. Derived from `endpoint` by default. */
7
+ checkEndpoint?: string;
8
+ /**
9
+ * Disable cap checking entirely. Off by default — the SDK will hit /api/v1/check
10
+ * before each LLM call (with a 60s per-user/plan cache).
11
+ */
12
+ disableCapCheck?: boolean;
6
13
  fetch?: typeof fetch;
7
14
  onError?: (err: unknown) => void;
8
15
  }
@@ -41,11 +48,21 @@ interface ProviderAdapter<TClient = unknown, TResult = unknown> {
41
48
  call(client: TClient, options: ChatOptions): Promise<TResult>;
42
49
  extractUsage(result: TResult): NormalizedUsage;
43
50
  }
51
+ interface CapCheckResult {
52
+ allowed: boolean;
53
+ action?: 'block' | 'downgrade';
54
+ alternativeModel?: string;
55
+ remainingBudget?: number;
56
+ currentSpend?: number;
57
+ cap?: number;
58
+ }
44
59
 
45
60
  declare class Weckr {
46
61
  private readonly apiKey;
47
62
  private readonly plans;
48
63
  private readonly log;
64
+ private readonly checkCap;
65
+ private readonly onError?;
49
66
  constructor(config: WeckrConfig);
50
67
  chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
51
68
  }
@@ -62,4 +79,34 @@ declare function calculateCost(model: string, inputTokens: number, outputTokens:
62
79
  provider: Provider | null;
63
80
  };
64
81
 
65
- export { type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, type WeckrConfig, calculateCost, resolvePricing };
82
+ /**
83
+ * Thrown by `wk.chat(...)` when the configured spending cap has been hit and
84
+ * the cap's action is `"block"`. The LLM call is never made.
85
+ *
86
+ * ```ts
87
+ * try {
88
+ * await wk.chat(openai, opts);
89
+ * } catch (err) {
90
+ * if (err instanceof WeckrCapError) {
91
+ * // show the user a friendly upgrade prompt
92
+ * }
93
+ * }
94
+ * ```
95
+ */
96
+ declare class WeckrCapError extends Error {
97
+ readonly name: "WeckrCapError";
98
+ readonly userId: string;
99
+ readonly planName: string;
100
+ readonly currentSpend?: number;
101
+ readonly cap?: number;
102
+ constructor(opts: {
103
+ userId: string;
104
+ planName: string;
105
+ currentSpend?: number;
106
+ cap?: number;
107
+ message?: string;
108
+ });
109
+ }
110
+ declare function isWeckrCapError(e: unknown): e is WeckrCapError;
111
+
112
+ export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, calculateCost, isWeckrCapError, resolvePricing };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,13 @@ interface WeckrConfig {
3
3
  apiKey: string;
4
4
  plans?: Record<string, number>;
5
5
  endpoint?: string;
6
+ /** Optional override for the cap-check endpoint. Derived from `endpoint` by default. */
7
+ checkEndpoint?: string;
8
+ /**
9
+ * Disable cap checking entirely. Off by default — the SDK will hit /api/v1/check
10
+ * before each LLM call (with a 60s per-user/plan cache).
11
+ */
12
+ disableCapCheck?: boolean;
6
13
  fetch?: typeof fetch;
7
14
  onError?: (err: unknown) => void;
8
15
  }
@@ -41,11 +48,21 @@ interface ProviderAdapter<TClient = unknown, TResult = unknown> {
41
48
  call(client: TClient, options: ChatOptions): Promise<TResult>;
42
49
  extractUsage(result: TResult): NormalizedUsage;
43
50
  }
51
+ interface CapCheckResult {
52
+ allowed: boolean;
53
+ action?: 'block' | 'downgrade';
54
+ alternativeModel?: string;
55
+ remainingBudget?: number;
56
+ currentSpend?: number;
57
+ cap?: number;
58
+ }
44
59
 
45
60
  declare class Weckr {
46
61
  private readonly apiKey;
47
62
  private readonly plans;
48
63
  private readonly log;
64
+ private readonly checkCap;
65
+ private readonly onError?;
49
66
  constructor(config: WeckrConfig);
50
67
  chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
51
68
  }
@@ -62,4 +79,34 @@ declare function calculateCost(model: string, inputTokens: number, outputTokens:
62
79
  provider: Provider | null;
63
80
  };
64
81
 
65
- export { type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, type WeckrConfig, calculateCost, resolvePricing };
82
+ /**
83
+ * Thrown by `wk.chat(...)` when the configured spending cap has been hit and
84
+ * the cap's action is `"block"`. The LLM call is never made.
85
+ *
86
+ * ```ts
87
+ * try {
88
+ * await wk.chat(openai, opts);
89
+ * } catch (err) {
90
+ * if (err instanceof WeckrCapError) {
91
+ * // show the user a friendly upgrade prompt
92
+ * }
93
+ * }
94
+ * ```
95
+ */
96
+ declare class WeckrCapError extends Error {
97
+ readonly name: "WeckrCapError";
98
+ readonly userId: string;
99
+ readonly planName: string;
100
+ readonly currentSpend?: number;
101
+ readonly cap?: number;
102
+ constructor(opts: {
103
+ userId: string;
104
+ planName: string;
105
+ currentSpend?: number;
106
+ cap?: number;
107
+ message?: string;
108
+ });
109
+ }
110
+ declare function isWeckrCapError(e: unknown): e is WeckrCapError;
111
+
112
+ export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, calculateCost, isWeckrCapError, resolvePricing };
package/dist/index.js CHANGED
@@ -22,7 +22,9 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  PRICING: () => PRICING,
24
24
  Weckr: () => Weckr,
25
+ WeckrCapError: () => WeckrCapError,
25
26
  calculateCost: () => calculateCost,
27
+ isWeckrCapError: () => isWeckrCapError,
26
28
  resolvePricing: () => resolvePricing
27
29
  });
28
30
  module.exports = __toCommonJS(index_exports);
@@ -34,7 +36,7 @@ var PRICING = {
34
36
  "gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
35
37
  "claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
36
38
  "claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
37
- "claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
39
+ "claude-haiku-4-5": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
38
40
  "gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
39
41
  "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
40
42
  };
@@ -164,24 +166,112 @@ function createLogger(opts) {
164
166
  };
165
167
  }
166
168
 
169
+ // src/cap-cache.ts
170
+ var TTL_MS = 6e4;
171
+ function createCapChecker(opts) {
172
+ const f = opts.fetch ?? globalThis.fetch;
173
+ const cache = /* @__PURE__ */ new Map();
174
+ function key(userId, planName) {
175
+ return `${userId}\0${planName}`;
176
+ }
177
+ return async function checkCap(userId, planName, model) {
178
+ const k = key(userId, planName);
179
+ const now = Date.now();
180
+ const hit = cache.get(k);
181
+ if (hit && hit.expiresAt > now) return hit.result;
182
+ if (typeof f !== "function") {
183
+ return { allowed: true };
184
+ }
185
+ try {
186
+ const url = new URL(opts.endpoint);
187
+ url.searchParams.set("userId", userId);
188
+ url.searchParams.set("planName", planName);
189
+ if (model) url.searchParams.set("model", model);
190
+ const res = await f(url.toString(), {
191
+ method: "GET",
192
+ headers: { "x-api-key": opts.apiKey }
193
+ });
194
+ if (!res.ok) {
195
+ opts.onError?.(
196
+ new Error(`Weckr cap check failed: ${res.status} ${res.statusText}`)
197
+ );
198
+ return { allowed: true };
199
+ }
200
+ const json = await res.json();
201
+ cache.set(k, { result: json, expiresAt: now + TTL_MS });
202
+ return json;
203
+ } catch (err) {
204
+ opts.onError?.(err);
205
+ return { allowed: true };
206
+ }
207
+ };
208
+ }
209
+ function deriveCheckEndpoint(logEndpoint) {
210
+ if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
211
+ return logEndpoint.replace(/\/$/, "") + "/../check";
212
+ }
213
+
214
+ // src/errors.ts
215
+ var WeckrCapError = class extends Error {
216
+ name = "WeckrCapError";
217
+ userId;
218
+ planName;
219
+ currentSpend;
220
+ cap;
221
+ constructor(opts) {
222
+ super(
223
+ opts.message ?? `Weckr: spending cap reached for user ${opts.userId} on plan ${opts.planName}`
224
+ );
225
+ this.userId = opts.userId;
226
+ this.planName = opts.planName;
227
+ this.currentSpend = opts.currentSpend;
228
+ this.cap = opts.cap;
229
+ }
230
+ };
231
+ function isWeckrCapError(e) {
232
+ return e instanceof Error && e.name === "WeckrCapError";
233
+ }
234
+ function capCheckToError(opts) {
235
+ return new WeckrCapError({
236
+ userId: opts.userId,
237
+ planName: opts.planName,
238
+ currentSpend: opts.result.currentSpend,
239
+ cap: opts.result.cap
240
+ });
241
+ }
242
+
167
243
  // src/weckr.ts
168
244
  var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
169
245
  var Weckr = class {
170
246
  apiKey;
171
247
  plans;
172
248
  log;
249
+ checkCap;
250
+ onError;
173
251
  constructor(config) {
174
252
  if (!config?.apiKey) {
175
253
  throw new Error("Weckr: apiKey is required.");
176
254
  }
177
255
  this.apiKey = config.apiKey;
178
256
  this.plans = config.plans ?? {};
257
+ this.onError = config.onError;
258
+ const logEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
179
259
  this.log = createLogger({
180
260
  apiKey: config.apiKey,
181
- endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
261
+ endpoint: logEndpoint,
182
262
  fetch: config.fetch,
183
263
  onError: config.onError
184
264
  });
265
+ if (config.disableCapCheck) {
266
+ this.checkCap = null;
267
+ } else {
268
+ this.checkCap = createCapChecker({
269
+ apiKey: config.apiKey,
270
+ endpoint: config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint),
271
+ fetch: config.fetch,
272
+ onError: config.onError
273
+ });
274
+ }
185
275
  }
186
276
  async chat(client, options) {
187
277
  const adapter = detectAdapter(client);
@@ -190,19 +280,39 @@ var Weckr = class {
190
280
  "Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
191
281
  );
192
282
  }
283
+ let effectiveOptions = options;
284
+ if (this.checkCap && options.userId && options.plan) {
285
+ const check = await this.checkCap(options.userId, options.plan, options.model);
286
+ if (!check.allowed) {
287
+ if (check.action === "downgrade" && check.alternativeModel) {
288
+ if (typeof console !== "undefined" && console.warn) {
289
+ console.warn(
290
+ `Weckr: downgrading ${options.userId} from ${options.model} to ${check.alternativeModel} (cap reached).`
291
+ );
292
+ }
293
+ effectiveOptions = { ...options, model: check.alternativeModel };
294
+ } else {
295
+ throw capCheckToError({
296
+ userId: options.userId,
297
+ planName: options.plan,
298
+ result: check
299
+ });
300
+ }
301
+ }
302
+ }
193
303
  const startedAt = nowMs();
194
- const result = await adapter.call(client, options);
304
+ const result = await adapter.call(client, effectiveOptions);
195
305
  const latencyMs = Math.round(nowMs() - startedAt);
196
306
  try {
197
307
  const usage = adapter.extractUsage(result);
198
- const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
199
- const planName = options.plan ?? null;
308
+ const { costUsd } = calculateCost(effectiveOptions.model, usage.inputTokens, usage.outputTokens);
309
+ const planName = effectiveOptions.plan ?? null;
200
310
  const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
201
311
  const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
202
312
  const payload = {
203
- userId: options.userId ?? null,
204
- feature: options.feature ?? null,
205
- model: options.model,
313
+ userId: effectiveOptions.userId ?? null,
314
+ feature: effectiveOptions.feature ?? null,
315
+ model: effectiveOptions.model,
206
316
  provider: adapter.name,
207
317
  inputTokens: usage.inputTokens,
208
318
  outputTokens: usage.outputTokens,
@@ -214,7 +324,8 @@ var Weckr = class {
214
324
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
215
325
  };
216
326
  this.log(payload);
217
- } catch {
327
+ } catch (err) {
328
+ this.onError?.(err);
218
329
  }
219
330
  return result;
220
331
  }
@@ -232,6 +343,8 @@ function round2(n) {
232
343
  0 && (module.exports = {
233
344
  PRICING,
234
345
  Weckr,
346
+ WeckrCapError,
235
347
  calculateCost,
348
+ isWeckrCapError,
236
349
  resolvePricing
237
350
  });
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ var PRICING = {
5
5
  "gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
6
6
  "claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
7
7
  "claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
8
- "claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
8
+ "claude-haiku-4-5": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
9
9
  "gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
10
10
  "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
11
11
  };
@@ -135,24 +135,112 @@ function createLogger(opts) {
135
135
  };
136
136
  }
137
137
 
138
+ // src/cap-cache.ts
139
+ var TTL_MS = 6e4;
140
+ function createCapChecker(opts) {
141
+ const f = opts.fetch ?? globalThis.fetch;
142
+ const cache = /* @__PURE__ */ new Map();
143
+ function key(userId, planName) {
144
+ return `${userId}\0${planName}`;
145
+ }
146
+ return async function checkCap(userId, planName, model) {
147
+ const k = key(userId, planName);
148
+ const now = Date.now();
149
+ const hit = cache.get(k);
150
+ if (hit && hit.expiresAt > now) return hit.result;
151
+ if (typeof f !== "function") {
152
+ return { allowed: true };
153
+ }
154
+ try {
155
+ const url = new URL(opts.endpoint);
156
+ url.searchParams.set("userId", userId);
157
+ url.searchParams.set("planName", planName);
158
+ if (model) url.searchParams.set("model", model);
159
+ const res = await f(url.toString(), {
160
+ method: "GET",
161
+ headers: { "x-api-key": opts.apiKey }
162
+ });
163
+ if (!res.ok) {
164
+ opts.onError?.(
165
+ new Error(`Weckr cap check failed: ${res.status} ${res.statusText}`)
166
+ );
167
+ return { allowed: true };
168
+ }
169
+ const json = await res.json();
170
+ cache.set(k, { result: json, expiresAt: now + TTL_MS });
171
+ return json;
172
+ } catch (err) {
173
+ opts.onError?.(err);
174
+ return { allowed: true };
175
+ }
176
+ };
177
+ }
178
+ function deriveCheckEndpoint(logEndpoint) {
179
+ if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
180
+ return logEndpoint.replace(/\/$/, "") + "/../check";
181
+ }
182
+
183
+ // src/errors.ts
184
+ var WeckrCapError = class extends Error {
185
+ name = "WeckrCapError";
186
+ userId;
187
+ planName;
188
+ currentSpend;
189
+ cap;
190
+ constructor(opts) {
191
+ super(
192
+ opts.message ?? `Weckr: spending cap reached for user ${opts.userId} on plan ${opts.planName}`
193
+ );
194
+ this.userId = opts.userId;
195
+ this.planName = opts.planName;
196
+ this.currentSpend = opts.currentSpend;
197
+ this.cap = opts.cap;
198
+ }
199
+ };
200
+ function isWeckrCapError(e) {
201
+ return e instanceof Error && e.name === "WeckrCapError";
202
+ }
203
+ function capCheckToError(opts) {
204
+ return new WeckrCapError({
205
+ userId: opts.userId,
206
+ planName: opts.planName,
207
+ currentSpend: opts.result.currentSpend,
208
+ cap: opts.result.cap
209
+ });
210
+ }
211
+
138
212
  // src/weckr.ts
139
213
  var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
140
214
  var Weckr = class {
141
215
  apiKey;
142
216
  plans;
143
217
  log;
218
+ checkCap;
219
+ onError;
144
220
  constructor(config) {
145
221
  if (!config?.apiKey) {
146
222
  throw new Error("Weckr: apiKey is required.");
147
223
  }
148
224
  this.apiKey = config.apiKey;
149
225
  this.plans = config.plans ?? {};
226
+ this.onError = config.onError;
227
+ const logEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
150
228
  this.log = createLogger({
151
229
  apiKey: config.apiKey,
152
- endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
230
+ endpoint: logEndpoint,
153
231
  fetch: config.fetch,
154
232
  onError: config.onError
155
233
  });
234
+ if (config.disableCapCheck) {
235
+ this.checkCap = null;
236
+ } else {
237
+ this.checkCap = createCapChecker({
238
+ apiKey: config.apiKey,
239
+ endpoint: config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint),
240
+ fetch: config.fetch,
241
+ onError: config.onError
242
+ });
243
+ }
156
244
  }
157
245
  async chat(client, options) {
158
246
  const adapter = detectAdapter(client);
@@ -161,19 +249,39 @@ var Weckr = class {
161
249
  "Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
162
250
  );
163
251
  }
252
+ let effectiveOptions = options;
253
+ if (this.checkCap && options.userId && options.plan) {
254
+ const check = await this.checkCap(options.userId, options.plan, options.model);
255
+ if (!check.allowed) {
256
+ if (check.action === "downgrade" && check.alternativeModel) {
257
+ if (typeof console !== "undefined" && console.warn) {
258
+ console.warn(
259
+ `Weckr: downgrading ${options.userId} from ${options.model} to ${check.alternativeModel} (cap reached).`
260
+ );
261
+ }
262
+ effectiveOptions = { ...options, model: check.alternativeModel };
263
+ } else {
264
+ throw capCheckToError({
265
+ userId: options.userId,
266
+ planName: options.plan,
267
+ result: check
268
+ });
269
+ }
270
+ }
271
+ }
164
272
  const startedAt = nowMs();
165
- const result = await adapter.call(client, options);
273
+ const result = await adapter.call(client, effectiveOptions);
166
274
  const latencyMs = Math.round(nowMs() - startedAt);
167
275
  try {
168
276
  const usage = adapter.extractUsage(result);
169
- const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
170
- const planName = options.plan ?? null;
277
+ const { costUsd } = calculateCost(effectiveOptions.model, usage.inputTokens, usage.outputTokens);
278
+ const planName = effectiveOptions.plan ?? null;
171
279
  const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
172
280
  const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
173
281
  const payload = {
174
- userId: options.userId ?? null,
175
- feature: options.feature ?? null,
176
- model: options.model,
282
+ userId: effectiveOptions.userId ?? null,
283
+ feature: effectiveOptions.feature ?? null,
284
+ model: effectiveOptions.model,
177
285
  provider: adapter.name,
178
286
  inputTokens: usage.inputTokens,
179
287
  outputTokens: usage.outputTokens,
@@ -185,7 +293,8 @@ var Weckr = class {
185
293
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
186
294
  };
187
295
  this.log(payload);
188
- } catch {
296
+ } catch (err) {
297
+ this.onError?.(err);
189
298
  }
190
299
  return result;
191
300
  }
@@ -202,6 +311,8 @@ function round2(n) {
202
311
  export {
203
312
  PRICING,
204
313
  Weckr,
314
+ WeckrCapError,
205
315
  calculateCost,
316
+ isWeckrCapError,
206
317
  resolvePricing
207
318
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weckr/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI cost and margin intelligence for SaaS founders",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -46,9 +46,15 @@
46
46
  "openai": ">=4.0.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
- "openai": { "optional": true },
50
- "@anthropic-ai/sdk": { "optional": true },
51
- "@google/generative-ai": { "optional": true }
49
+ "openai": {
50
+ "optional": true
51
+ },
52
+ "@anthropic-ai/sdk": {
53
+ "optional": true
54
+ },
55
+ "@google/generative-ai": {
56
+ "optional": true
57
+ }
52
58
  },
53
59
  "devDependencies": {
54
60
  "@types/node": "^22.10.0",