@weckr/sdk 0.1.1 → 0.1.3

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
@@ -45,9 +45,41 @@ Sign up at [https://useweckr.com](https://useweckr.com).
45
45
 
46
46
  ## Supported providers
47
47
 
48
- - **OpenAI** — `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`
49
- - **Anthropic** — `claude-opus-4`, `claude-sonnet-4`, `claude-haiku-4`
50
- - **Gemini** — `gemini-2.5-flash`, `gemini-2.5-pro`
48
+ - **OpenAI** — `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`, `gpt-4`, `gpt-3.5-turbo`, `o1-preview`, `o1-mini`
49
+ - **Anthropic** — `claude-opus-4`, `claude-sonnet-4`, `claude-haiku-4-5`, `claude-3-5-sonnet`, `claude-3-5-haiku`, `claude-3-opus`
50
+ - **Gemini** — `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-1.5-pro`, `gemini-1.5-flash`
51
+
52
+ Dated variants (`gpt-4o-2024-08-06`, `claude-3-5-sonnet-latest`, …) resolve to the matching family by longest-prefix lookup. Unknown models log `costUsd = 0` and don't trigger caps.
53
+
54
+ ## Caps + downgrades
55
+
56
+ Set per-plan spending caps in the dashboard. When a user crosses their cap:
57
+
58
+ - `action: 'block'` → `wk.chat()` throws `WeckrCapError` (LLM call never made)
59
+ - `action: 'downgrade'` → the SDK silently swaps the model for a cheaper one in the same provider (`gpt-4o` → `gpt-4o-mini`, `claude-opus-4` → `claude-sonnet-4`, etc.)
60
+
61
+ Errors come in two flavors:
62
+
63
+ ```ts
64
+ import { isWeckrCapError, isWeckrConfigError } from '@weckr/sdk';
65
+
66
+ try {
67
+ await wk.chat(openai, opts);
68
+ } catch (err) {
69
+ if (isWeckrCapError(err)) return showUpgradePrompt(err);
70
+ if (isWeckrConfigError(err)) return logBackendAlert(err); // typo'd api key, unknown plan
71
+ throw err; // real LLM error
72
+ }
73
+ ```
74
+
75
+ ## Short-lived processes (Lambda, cron, CLI)
76
+
77
+ `wk.chat()` returns as soon as the LLM call resolves; the log POST is fire-and-forget. In short-lived processes call `await wk.flush()` before exit to give the POSTs time to land:
78
+
79
+ ```ts
80
+ await wk.chat(openai, opts);
81
+ await wk.flush(); // default 5s timeout
82
+ ```
51
83
 
52
84
  ## What gets logged
53
85
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,12 @@
1
- type Provider = 'openai' | 'anthropic' | 'gemini';
1
+ /**
2
+ * 'unknown' is used when the SDK can't detect the provider via shape. We still
3
+ * log the row (so the dashboard sees something) but cost+downgrade lookup fall
4
+ * back to no-op behavior.
5
+ */
6
+ type Provider = 'openai' | 'anthropic' | 'gemini' | 'unknown';
2
7
  interface WeckrConfig {
3
8
  apiKey: string;
9
+ /** Map of plan name -> monthly revenue per user. Required if you pass `plan` to chat(). */
4
10
  plans?: Record<string, number>;
5
11
  endpoint?: string;
6
12
  /** Optional override for the cap-check endpoint. Derived from `endpoint` by default. */
@@ -11,17 +17,33 @@ interface WeckrConfig {
11
17
  */
12
18
  disableCapCheck?: boolean;
13
19
  fetch?: typeof fetch;
20
+ /**
21
+ * Async errors (cap-check network failure, log POST failure) are reported here.
22
+ * If absent, errors are silently swallowed. CRITICAL ones (401/403 on a
23
+ * misconfigured api key) still throw WeckrConfigError synchronously.
24
+ */
14
25
  onError?: (err: unknown) => void;
26
+ /**
27
+ * Called when a cap-downgrade swaps the model. Useful for analytics.
28
+ * Defaults to a one-time console.warn per (userId, from, to).
29
+ */
30
+ onDowngrade?: (info: {
31
+ userId: string;
32
+ from: string;
33
+ to: string;
34
+ }) => void;
15
35
  }
16
36
  interface ChatOptions {
17
37
  model: string;
18
38
  messages: Array<{
19
39
  role: string;
20
- content: string;
40
+ content: unknown;
21
41
  }>;
22
42
  userId?: string;
23
43
  feature?: string;
24
44
  plan?: string;
45
+ /** OpenAI streaming opt-in — set to true to get an AsyncIterable response. */
46
+ stream?: boolean;
25
47
  [key: string]: unknown;
26
48
  }
27
49
  interface NormalizedUsage {
@@ -39,6 +61,7 @@ interface LogPayload {
39
61
  latencyMs: number;
40
62
  planName: string | null;
41
63
  planRevenueUsd: number | null;
64
+ /** Kept for backward-compat with old servers; new servers ignore it. */
42
65
  marginUsd: number | null;
43
66
  timestamp: string;
44
67
  }
@@ -60,11 +83,20 @@ interface CapCheckResult {
60
83
  declare class Weckr {
61
84
  private readonly apiKey;
62
85
  private readonly plans;
63
- private readonly log;
86
+ private readonly logger;
64
87
  private readonly checkCap;
65
88
  private readonly onError?;
89
+ private readonly onDowngrade;
90
+ private readonly downgradeSeen;
66
91
  constructor(config: WeckrConfig);
67
92
  chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
93
+ private tryLog;
94
+ /**
95
+ * Await all in-flight log POSTs. Call this before `process.exit()` /
96
+ * `Lambda return` / end of a short-lived CLI run, otherwise the daemon
97
+ * process is torn down before the POST hits the network.
98
+ */
99
+ flush(timeoutMs?: number): Promise<void>;
68
100
  }
69
101
 
70
102
  interface ModelPricing {
@@ -73,6 +105,15 @@ interface ModelPricing {
73
105
  outputPerMillion: number;
74
106
  }
75
107
  declare const PRICING: Record<string, ModelPricing>;
108
+ /**
109
+ * Resolve pricing for a model name, allowing dated variants.
110
+ *
111
+ * Real-world IDs are date-pinned (`gpt-4o-2024-08-06`, `claude-opus-4-20250514`,
112
+ * `claude-3-5-sonnet-latest`). Strict equality would silently log cost=0 for
113
+ * those — which neuters every cap. So we longest-prefix-match against PRICING:
114
+ * `claude-3-5-sonnet-20241022` resolves to `claude-3-5-sonnet`, not the
115
+ * shorter `claude-3` family.
116
+ */
76
117
  declare function resolvePricing(model: string): ModelPricing | null;
77
118
  declare function calculateCost(model: string, inputTokens: number, outputTokens: number): {
78
119
  costUsd: number;
@@ -87,7 +128,7 @@ declare function calculateCost(model: string, inputTokens: number, outputTokens:
87
128
  * try {
88
129
  * await wk.chat(openai, opts);
89
130
  * } catch (err) {
90
- * if (err instanceof WeckrCapError) {
131
+ * if (isWeckrCapError(err)) {
91
132
  * // show the user a friendly upgrade prompt
92
133
  * }
93
134
  * }
@@ -108,5 +149,19 @@ declare class WeckrCapError extends Error {
108
149
  });
109
150
  }
110
151
  declare function isWeckrCapError(e: unknown): e is WeckrCapError;
152
+ /**
153
+ * Thrown when the SDK detects an UNRECOVERABLE config error — typo'd api key
154
+ * (401), revoked key (403), or plan-name passed to chat() that doesn't appear
155
+ * in the constructor's `plans` dict.
156
+ *
157
+ * These fail-CLOSED on purpose: silent fail-open would silently disable cap
158
+ * enforcement (security control) or silently poison dashboard data.
159
+ */
160
+ declare class WeckrConfigError extends Error {
161
+ readonly name: "WeckrConfigError";
162
+ readonly code: 'invalid_api_key' | 'forbidden' | 'unknown_plan';
163
+ constructor(code: 'invalid_api_key' | 'forbidden' | 'unknown_plan', message: string);
164
+ }
165
+ declare function isWeckrConfigError(e: unknown): e is WeckrConfigError;
111
166
 
112
- export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, calculateCost, isWeckrCapError, resolvePricing };
167
+ export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, WeckrConfigError, calculateCost, isWeckrCapError, isWeckrConfigError, resolvePricing };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
- type Provider = 'openai' | 'anthropic' | 'gemini';
1
+ /**
2
+ * 'unknown' is used when the SDK can't detect the provider via shape. We still
3
+ * log the row (so the dashboard sees something) but cost+downgrade lookup fall
4
+ * back to no-op behavior.
5
+ */
6
+ type Provider = 'openai' | 'anthropic' | 'gemini' | 'unknown';
2
7
  interface WeckrConfig {
3
8
  apiKey: string;
9
+ /** Map of plan name -> monthly revenue per user. Required if you pass `plan` to chat(). */
4
10
  plans?: Record<string, number>;
5
11
  endpoint?: string;
6
12
  /** Optional override for the cap-check endpoint. Derived from `endpoint` by default. */
@@ -11,17 +17,33 @@ interface WeckrConfig {
11
17
  */
12
18
  disableCapCheck?: boolean;
13
19
  fetch?: typeof fetch;
20
+ /**
21
+ * Async errors (cap-check network failure, log POST failure) are reported here.
22
+ * If absent, errors are silently swallowed. CRITICAL ones (401/403 on a
23
+ * misconfigured api key) still throw WeckrConfigError synchronously.
24
+ */
14
25
  onError?: (err: unknown) => void;
26
+ /**
27
+ * Called when a cap-downgrade swaps the model. Useful for analytics.
28
+ * Defaults to a one-time console.warn per (userId, from, to).
29
+ */
30
+ onDowngrade?: (info: {
31
+ userId: string;
32
+ from: string;
33
+ to: string;
34
+ }) => void;
15
35
  }
16
36
  interface ChatOptions {
17
37
  model: string;
18
38
  messages: Array<{
19
39
  role: string;
20
- content: string;
40
+ content: unknown;
21
41
  }>;
22
42
  userId?: string;
23
43
  feature?: string;
24
44
  plan?: string;
45
+ /** OpenAI streaming opt-in — set to true to get an AsyncIterable response. */
46
+ stream?: boolean;
25
47
  [key: string]: unknown;
26
48
  }
27
49
  interface NormalizedUsage {
@@ -39,6 +61,7 @@ interface LogPayload {
39
61
  latencyMs: number;
40
62
  planName: string | null;
41
63
  planRevenueUsd: number | null;
64
+ /** Kept for backward-compat with old servers; new servers ignore it. */
42
65
  marginUsd: number | null;
43
66
  timestamp: string;
44
67
  }
@@ -60,11 +83,20 @@ interface CapCheckResult {
60
83
  declare class Weckr {
61
84
  private readonly apiKey;
62
85
  private readonly plans;
63
- private readonly log;
86
+ private readonly logger;
64
87
  private readonly checkCap;
65
88
  private readonly onError?;
89
+ private readonly onDowngrade;
90
+ private readonly downgradeSeen;
66
91
  constructor(config: WeckrConfig);
67
92
  chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
93
+ private tryLog;
94
+ /**
95
+ * Await all in-flight log POSTs. Call this before `process.exit()` /
96
+ * `Lambda return` / end of a short-lived CLI run, otherwise the daemon
97
+ * process is torn down before the POST hits the network.
98
+ */
99
+ flush(timeoutMs?: number): Promise<void>;
68
100
  }
69
101
 
70
102
  interface ModelPricing {
@@ -73,6 +105,15 @@ interface ModelPricing {
73
105
  outputPerMillion: number;
74
106
  }
75
107
  declare const PRICING: Record<string, ModelPricing>;
108
+ /**
109
+ * Resolve pricing for a model name, allowing dated variants.
110
+ *
111
+ * Real-world IDs are date-pinned (`gpt-4o-2024-08-06`, `claude-opus-4-20250514`,
112
+ * `claude-3-5-sonnet-latest`). Strict equality would silently log cost=0 for
113
+ * those — which neuters every cap. So we longest-prefix-match against PRICING:
114
+ * `claude-3-5-sonnet-20241022` resolves to `claude-3-5-sonnet`, not the
115
+ * shorter `claude-3` family.
116
+ */
76
117
  declare function resolvePricing(model: string): ModelPricing | null;
77
118
  declare function calculateCost(model: string, inputTokens: number, outputTokens: number): {
78
119
  costUsd: number;
@@ -87,7 +128,7 @@ declare function calculateCost(model: string, inputTokens: number, outputTokens:
87
128
  * try {
88
129
  * await wk.chat(openai, opts);
89
130
  * } catch (err) {
90
- * if (err instanceof WeckrCapError) {
131
+ * if (isWeckrCapError(err)) {
91
132
  * // show the user a friendly upgrade prompt
92
133
  * }
93
134
  * }
@@ -108,5 +149,19 @@ declare class WeckrCapError extends Error {
108
149
  });
109
150
  }
110
151
  declare function isWeckrCapError(e: unknown): e is WeckrCapError;
152
+ /**
153
+ * Thrown when the SDK detects an UNRECOVERABLE config error — typo'd api key
154
+ * (401), revoked key (403), or plan-name passed to chat() that doesn't appear
155
+ * in the constructor's `plans` dict.
156
+ *
157
+ * These fail-CLOSED on purpose: silent fail-open would silently disable cap
158
+ * enforcement (security control) or silently poison dashboard data.
159
+ */
160
+ declare class WeckrConfigError extends Error {
161
+ readonly name: "WeckrConfigError";
162
+ readonly code: 'invalid_api_key' | 'forbidden' | 'unknown_plan';
163
+ constructor(code: 'invalid_api_key' | 'forbidden' | 'unknown_plan', message: string);
164
+ }
165
+ declare function isWeckrConfigError(e: unknown): e is WeckrConfigError;
111
166
 
112
- export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, calculateCost, isWeckrCapError, resolvePricing };
167
+ export { type CapCheckResult, type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, WeckrCapError, type WeckrConfig, WeckrConfigError, calculateCost, isWeckrCapError, isWeckrConfigError, resolvePricing };
package/dist/index.js CHANGED
@@ -23,30 +23,47 @@ __export(index_exports, {
23
23
  PRICING: () => PRICING,
24
24
  Weckr: () => Weckr,
25
25
  WeckrCapError: () => WeckrCapError,
26
+ WeckrConfigError: () => WeckrConfigError,
26
27
  calculateCost: () => calculateCost,
27
28
  isWeckrCapError: () => isWeckrCapError,
29
+ isWeckrConfigError: () => isWeckrConfigError,
28
30
  resolvePricing: () => resolvePricing
29
31
  });
30
32
  module.exports = __toCommonJS(index_exports);
31
33
 
32
34
  // src/pricing.ts
33
35
  var PRICING = {
36
+ // OpenAI
34
37
  "gpt-4o": { provider: "openai", inputPerMillion: 2.5, outputPerMillion: 10 },
35
38
  "gpt-4o-mini": { provider: "openai", inputPerMillion: 0.15, outputPerMillion: 0.6 },
39
+ "gpt-4-turbo": { provider: "openai", inputPerMillion: 10, outputPerMillion: 30 },
40
+ "gpt-4": { provider: "openai", inputPerMillion: 30, outputPerMillion: 60 },
36
41
  "gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
42
+ "o1-preview": { provider: "openai", inputPerMillion: 15, outputPerMillion: 60 },
43
+ "o1-mini": { provider: "openai", inputPerMillion: 3, outputPerMillion: 12 },
44
+ // Anthropic
37
45
  "claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
38
46
  "claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
39
- "claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
47
+ "claude-haiku-4-5": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
48
+ "claude-3-5-sonnet": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
49
+ "claude-3-5-haiku": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
50
+ "claude-3-opus": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
51
+ // Gemini
52
+ "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 },
40
53
  "gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
41
- "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
54
+ "gemini-1.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 5 },
55
+ "gemini-1.5-flash": { provider: "gemini", inputPerMillion: 0.075, outputPerMillion: 0.3 }
42
56
  };
43
57
  function resolvePricing(model) {
44
58
  if (PRICING[model]) return PRICING[model];
45
59
  const lower = model.toLowerCase();
46
- for (const key of Object.keys(PRICING)) {
47
- if (lower.startsWith(key.toLowerCase())) return PRICING[key];
60
+ let best = null;
61
+ for (const [key, pricing] of Object.entries(PRICING)) {
62
+ if (lower.startsWith(key.toLowerCase())) {
63
+ if (!best || key.length > best.key.length) best = { key, pricing };
64
+ }
48
65
  }
49
- return null;
66
+ return best?.pricing ?? null;
50
67
  }
51
68
  function calculateCost(model, inputTokens, outputTokens) {
52
69
  const pricing = resolvePricing(model);
@@ -62,14 +79,28 @@ function round6(n) {
62
79
  var openaiAdapter = {
63
80
  name: "openai",
64
81
  matches(client) {
65
- return isObject(client) && isObject(client.chat) && isObject(client.chat.completions) && typeof client.chat.completions.create === "function";
82
+ if (!isObject(client)) return false;
83
+ const chat = client.chat;
84
+ if (!isObject(chat)) return false;
85
+ const completions = chat.completions;
86
+ if (!isObject(completions)) return false;
87
+ return typeof completions.create === "function";
66
88
  },
67
89
  async call(client, options) {
90
+ if (options.stream === true) {
91
+ throw new Error(
92
+ "Weckr: stream:true is not supported by wk.chat() because token usage is not in stream responses by default. Either disable streaming, or call openai.chat.completions.create() directly outside wk.chat() (you lose cost tracking) and we will add proper streaming support in a future release."
93
+ );
94
+ }
68
95
  const { userId, feature, plan, ...rest } = options;
96
+ void userId;
97
+ void feature;
98
+ void plan;
69
99
  return client.chat.completions.create(rest);
70
100
  },
71
101
  extractUsage(result) {
72
- const usage = result?.usage ?? {};
102
+ const r = result;
103
+ const usage = r?.usage ?? {};
73
104
  return {
74
105
  inputTokens: toInt(usage.prompt_tokens ?? usage.input_tokens),
75
106
  outputTokens: toInt(usage.completion_tokens ?? usage.output_tokens)
@@ -79,14 +110,21 @@ var openaiAdapter = {
79
110
  var anthropicAdapter = {
80
111
  name: "anthropic",
81
112
  matches(client) {
82
- return isObject(client) && isObject(client.messages) && typeof client.messages.create === "function";
113
+ if (!isObject(client)) return false;
114
+ const messages = client.messages;
115
+ if (!isObject(messages)) return false;
116
+ return typeof messages.create === "function";
83
117
  },
84
118
  async call(client, options) {
85
119
  const { userId, feature, plan, ...rest } = options;
120
+ void userId;
121
+ void feature;
122
+ void plan;
86
123
  return client.messages.create(rest);
87
124
  },
88
125
  extractUsage(result) {
89
- const usage = result?.usage ?? {};
126
+ const r = result;
127
+ const usage = r?.usage ?? {};
90
128
  return {
91
129
  inputTokens: toInt(usage.input_tokens),
92
130
  outputTokens: toInt(usage.output_tokens)
@@ -96,18 +134,25 @@ var anthropicAdapter = {
96
134
  var geminiAdapter = {
97
135
  name: "gemini",
98
136
  matches(client) {
99
- return isObject(client) && isObject(client.models) && typeof client.models.generateContent === "function";
137
+ if (!isObject(client)) return false;
138
+ const models = client.models;
139
+ if (!isObject(models)) return false;
140
+ return typeof models.generateContent === "function";
100
141
  },
101
142
  async call(client, options) {
102
143
  const { userId, feature, plan, messages, ...rest } = options;
144
+ void userId;
145
+ void feature;
146
+ void plan;
103
147
  const contents = (messages ?? []).map((m) => ({
104
148
  role: m.role === "assistant" ? "model" : "user",
105
- parts: [{ text: m.content }]
149
+ parts: [{ text: typeof m.content === "string" ? m.content : JSON.stringify(m.content) }]
106
150
  }));
107
151
  return client.models.generateContent({ ...rest, contents });
108
152
  },
109
153
  extractUsage(result) {
110
- const meta = result?.usageMetadata ?? {};
154
+ const r = result;
155
+ const meta = r?.usageMetadata ?? {};
111
156
  return {
112
157
  inputTokens: toInt(meta.promptTokenCount),
113
158
  outputTokens: toInt(meta.candidatesTokenCount)
@@ -131,11 +176,15 @@ function toInt(v) {
131
176
 
132
177
  // src/logger.ts
133
178
  function createLogger(opts) {
134
- const f = opts.fetch ?? globalThis.fetch;
135
- if (typeof f !== "function") {
136
- throw new Error("Weckr: global fetch is unavailable. Pass a fetch implementation via config.fetch.");
137
- }
138
- return function log(payload) {
179
+ const inflight = /* @__PURE__ */ new Set();
180
+ function log(payload) {
181
+ const f = opts.fetch ?? globalThis.fetch;
182
+ if (typeof f !== "function") {
183
+ opts.onError?.(
184
+ new Error("Weckr: global fetch is unavailable. Pass a fetch implementation via config.fetch.")
185
+ );
186
+ return;
187
+ }
139
188
  queueMicrotask(() => {
140
189
  let promise;
141
190
  try {
@@ -152,63 +201,38 @@ function createLogger(opts) {
152
201
  opts.onError?.(err);
153
202
  return;
154
203
  }
155
- promise.then(async (res) => {
204
+ const tracked = promise.then(async (res) => {
156
205
  if (!res.ok) {
157
206
  const body = await res.text().catch(() => "");
158
207
  opts.onError?.(
159
- new Error(`Weckr log failed: ${res.status} ${res.statusText} ${body}`.trim())
208
+ new Error(
209
+ `Weckr log failed: ${res.status} ${res.statusText} ${body}. ` + (res.status === 401 || res.status === 403 ? `Verify the api key at https://app.useweckr.com/dashboard/settings.` : "")
210
+ )
160
211
  );
161
212
  }
162
213
  }).catch((err) => {
163
214
  opts.onError?.(err);
215
+ }).finally(() => {
216
+ inflight.delete(tracked);
164
217
  });
218
+ inflight.add(tracked);
165
219
  });
166
- };
167
- }
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
220
  }
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 };
221
+ async function flush(timeoutMs = 5e3) {
222
+ if (inflight.size === 0) return;
223
+ const all = Promise.allSettled(Array.from(inflight));
224
+ if (timeoutMs <= 0) {
225
+ await all;
226
+ return;
206
227
  }
207
- };
208
- }
209
- function deriveCheckEndpoint(logEndpoint) {
210
- if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
211
- return logEndpoint.replace(/\/$/, "") + "/../check";
228
+ let timer;
229
+ const timeout = new Promise((resolve) => {
230
+ timer = setTimeout(() => resolve(), timeoutMs);
231
+ });
232
+ await Promise.race([all, timeout]);
233
+ if (timer) clearTimeout(timer);
234
+ }
235
+ return { log, flush };
212
236
  }
213
237
 
214
238
  // src/errors.ts
@@ -239,15 +263,94 @@ function capCheckToError(opts) {
239
263
  cap: opts.result.cap
240
264
  });
241
265
  }
266
+ var WeckrConfigError = class extends Error {
267
+ name = "WeckrConfigError";
268
+ code;
269
+ constructor(code, message) {
270
+ super(message);
271
+ this.code = code;
272
+ }
273
+ };
274
+ function isWeckrConfigError(e) {
275
+ return e instanceof Error && e.name === "WeckrConfigError";
276
+ }
277
+
278
+ // src/cap-cache.ts
279
+ var TTL_MS = 6e4;
280
+ function cacheKey(userId, planName, model) {
281
+ return JSON.stringify([userId, planName, model ?? null]);
282
+ }
283
+ function createCapChecker(opts) {
284
+ const f = opts.fetch ?? globalThis.fetch;
285
+ const cache = /* @__PURE__ */ new Map();
286
+ const inflight = /* @__PURE__ */ new Map();
287
+ return async function checkCap(userId, planName, model) {
288
+ const k = cacheKey(userId, planName, model);
289
+ const now = Date.now();
290
+ const hit = cache.get(k);
291
+ if (hit && hit.expiresAt > now) return hit.result;
292
+ const pending = inflight.get(k);
293
+ if (pending) return pending;
294
+ if (typeof f !== "function") {
295
+ return { allowed: true };
296
+ }
297
+ const fetchPromise = (async () => {
298
+ try {
299
+ const url = new URL(opts.endpoint);
300
+ url.searchParams.set("userId", userId);
301
+ url.searchParams.set("planName", planName);
302
+ if (model) url.searchParams.set("model", model);
303
+ const res = await f(url.toString(), {
304
+ method: "GET",
305
+ headers: { "x-api-key": opts.apiKey }
306
+ });
307
+ if (res.status === 401 || res.status === 403) {
308
+ const body = await res.text().catch(() => "");
309
+ throw new WeckrConfigError(
310
+ res.status === 401 ? "invalid_api_key" : "forbidden",
311
+ `Weckr: cap-check rejected with ${res.status}. Verify the api key is correct, not revoked, and active in the dashboard. Server said: ${body || "(no body)"}`
312
+ );
313
+ }
314
+ if (!res.ok) {
315
+ opts.onError?.(
316
+ new Error(`Weckr cap check failed: ${res.status} ${res.statusText}`)
317
+ );
318
+ return { allowed: true };
319
+ }
320
+ const json = await res.json();
321
+ cache.set(k, { result: json, expiresAt: Date.now() + TTL_MS });
322
+ return json;
323
+ } catch (err) {
324
+ if (err instanceof WeckrConfigError) throw err;
325
+ opts.onError?.(err);
326
+ return { allowed: true };
327
+ } finally {
328
+ inflight.delete(k);
329
+ }
330
+ })();
331
+ inflight.set(k, fetchPromise);
332
+ return fetchPromise;
333
+ };
334
+ }
335
+ function deriveCheckEndpoint(logEndpoint) {
336
+ if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
337
+ throw new WeckrConfigError(
338
+ "invalid_api_key",
339
+ // closest reusable code; semantically a config bug
340
+ `Weckr: cannot derive checkEndpoint from endpoint "${logEndpoint}" \u2014 it does not end in "/log". Pass an explicit \`checkEndpoint\` in the Weckr config.`
341
+ );
342
+ }
242
343
 
243
344
  // src/weckr.ts
244
- var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
345
+ var DEFAULT_ENDPOINT = "https://app.useweckr.com/api/v1/log";
245
346
  var Weckr = class {
246
347
  apiKey;
247
348
  plans;
248
- log;
349
+ logger;
249
350
  checkCap;
250
351
  onError;
352
+ onDowngrade;
353
+ downgradeSeen = /* @__PURE__ */ new Set();
251
354
  constructor(config) {
252
355
  if (!config?.apiKey) {
253
356
  throw new Error("Weckr: apiKey is required.");
@@ -255,8 +358,9 @@ var Weckr = class {
255
358
  this.apiKey = config.apiKey;
256
359
  this.plans = config.plans ?? {};
257
360
  this.onError = config.onError;
361
+ this.onDowngrade = config.onDowngrade ?? defaultDowngradeWarn.bind(this);
258
362
  const logEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
259
- this.log = createLogger({
363
+ this.logger = createLogger({
260
364
  apiKey: config.apiKey,
261
365
  endpoint: logEndpoint,
262
366
  fetch: config.fetch,
@@ -265,9 +369,10 @@ var Weckr = class {
265
369
  if (config.disableCapCheck) {
266
370
  this.checkCap = null;
267
371
  } else {
372
+ const checkEndpoint = config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint);
268
373
  this.checkCap = createCapChecker({
269
374
  apiKey: config.apiKey,
270
- endpoint: config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint),
375
+ endpoint: checkEndpoint,
271
376
  fetch: config.fetch,
272
377
  onError: config.onError
273
378
  });
@@ -280,16 +385,22 @@ var Weckr = class {
280
385
  "Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
281
386
  );
282
387
  }
388
+ if (options.plan != null && !Object.prototype.hasOwnProperty.call(this.plans, options.plan)) {
389
+ throw new WeckrConfigError(
390
+ "unknown_plan",
391
+ `Weckr: plan "${options.plan}" is not in the constructor's \`plans\` map. Add it as \`plans: { "${options.plan}": <monthly_usd> }\` when constructing Weckr.`
392
+ );
393
+ }
283
394
  let effectiveOptions = options;
284
395
  if (this.checkCap && options.userId && options.plan) {
285
396
  const check = await this.checkCap(options.userId, options.plan, options.model);
286
397
  if (!check.allowed) {
287
398
  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
- }
399
+ this.onDowngrade({
400
+ userId: options.userId,
401
+ from: options.model,
402
+ to: check.alternativeModel
403
+ });
293
404
  effectiveOptions = { ...options, model: check.alternativeModel };
294
405
  } else {
295
406
  throw capCheckToError({
@@ -301,19 +412,40 @@ var Weckr = class {
301
412
  }
302
413
  }
303
414
  const startedAt = nowMs();
304
- const result = await adapter.call(client, effectiveOptions);
415
+ let result;
416
+ try {
417
+ result = await adapter.call(client, effectiveOptions);
418
+ } catch (err) {
419
+ const latencyMs2 = Math.round(nowMs() - startedAt);
420
+ this.tryLog(
421
+ adapter.name,
422
+ effectiveOptions,
423
+ { inputTokens: 0, outputTokens: 0 },
424
+ latencyMs2
425
+ );
426
+ throw err;
427
+ }
305
428
  const latencyMs = Math.round(nowMs() - startedAt);
429
+ let usage = { inputTokens: 0, outputTokens: 0 };
306
430
  try {
307
- const usage = adapter.extractUsage(result);
308
- const { costUsd } = calculateCost(effectiveOptions.model, usage.inputTokens, usage.outputTokens);
309
- const planName = effectiveOptions.plan ?? null;
431
+ usage = adapter.extractUsage(result);
432
+ } catch (err) {
433
+ this.onError?.(err);
434
+ }
435
+ this.tryLog(adapter.name, effectiveOptions, usage, latencyMs);
436
+ return result;
437
+ }
438
+ tryLog(provider, options, usage, latencyMs) {
439
+ try {
440
+ const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
441
+ const planName = options.plan ?? null;
310
442
  const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
311
- const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
443
+ const marginUsd = planRevenueUsd != null ? planRevenueUsd - costUsd : null;
312
444
  const payload = {
313
- userId: effectiveOptions.userId ?? null,
314
- feature: effectiveOptions.feature ?? null,
315
- model: effectiveOptions.model,
316
- provider: adapter.name,
445
+ userId: options.userId ?? null,
446
+ feature: options.feature ?? null,
447
+ model: options.model,
448
+ provider,
317
449
  inputTokens: usage.inputTokens,
318
450
  outputTokens: usage.outputTokens,
319
451
  costUsd,
@@ -321,30 +453,48 @@ var Weckr = class {
321
453
  planName,
322
454
  planRevenueUsd,
323
455
  marginUsd,
456
+ // sent for backward-compat; server ignores
324
457
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
325
458
  };
326
- this.log(payload);
459
+ this.logger.log(payload);
327
460
  } catch (err) {
328
461
  this.onError?.(err);
329
462
  }
330
- return result;
463
+ }
464
+ /**
465
+ * Await all in-flight log POSTs. Call this before `process.exit()` /
466
+ * `Lambda return` / end of a short-lived CLI run, otherwise the daemon
467
+ * process is torn down before the POST hits the network.
468
+ */
469
+ flush(timeoutMs) {
470
+ return this.logger.flush(timeoutMs);
331
471
  }
332
472
  };
473
+ function defaultDowngradeWarn(info) {
474
+ const key = `${info.userId}:${info.from}>${info.to}`;
475
+ const seen = this.downgradeSeen;
476
+ if (seen.has(key)) return;
477
+ seen.add(key);
478
+ if (typeof console !== "undefined" && console.warn) {
479
+ console.warn(
480
+ `Weckr: downgrading ${info.userId} from ${info.from} to ${info.to} (cap reached). Subsequent downgrades for this user/model will be silent.`
481
+ );
482
+ }
483
+ }
333
484
  function nowMs() {
334
485
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
335
486
  return performance.now();
336
487
  }
337
488
  return Date.now();
338
489
  }
339
- function round2(n) {
340
- return Math.round(n * 100) / 100;
341
- }
342
490
  // Annotate the CommonJS export names for ESM import in node:
343
491
  0 && (module.exports = {
344
492
  PRICING,
345
493
  Weckr,
346
494
  WeckrCapError,
495
+ WeckrConfigError,
347
496
  calculateCost,
348
497
  isWeckrCapError,
498
+ isWeckrConfigError,
349
499
  resolvePricing
350
500
  });
package/dist/index.mjs CHANGED
@@ -1,21 +1,36 @@
1
1
  // src/pricing.ts
2
2
  var PRICING = {
3
+ // OpenAI
3
4
  "gpt-4o": { provider: "openai", inputPerMillion: 2.5, outputPerMillion: 10 },
4
5
  "gpt-4o-mini": { provider: "openai", inputPerMillion: 0.15, outputPerMillion: 0.6 },
6
+ "gpt-4-turbo": { provider: "openai", inputPerMillion: 10, outputPerMillion: 30 },
7
+ "gpt-4": { provider: "openai", inputPerMillion: 30, outputPerMillion: 60 },
5
8
  "gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
9
+ "o1-preview": { provider: "openai", inputPerMillion: 15, outputPerMillion: 60 },
10
+ "o1-mini": { provider: "openai", inputPerMillion: 3, outputPerMillion: 12 },
11
+ // Anthropic
6
12
  "claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
7
13
  "claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
8
- "claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
14
+ "claude-haiku-4-5": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
15
+ "claude-3-5-sonnet": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
16
+ "claude-3-5-haiku": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
17
+ "claude-3-opus": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
18
+ // Gemini
19
+ "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 },
9
20
  "gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
10
- "gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
21
+ "gemini-1.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 5 },
22
+ "gemini-1.5-flash": { provider: "gemini", inputPerMillion: 0.075, outputPerMillion: 0.3 }
11
23
  };
12
24
  function resolvePricing(model) {
13
25
  if (PRICING[model]) return PRICING[model];
14
26
  const lower = model.toLowerCase();
15
- for (const key of Object.keys(PRICING)) {
16
- if (lower.startsWith(key.toLowerCase())) return PRICING[key];
27
+ let best = null;
28
+ for (const [key, pricing] of Object.entries(PRICING)) {
29
+ if (lower.startsWith(key.toLowerCase())) {
30
+ if (!best || key.length > best.key.length) best = { key, pricing };
31
+ }
17
32
  }
18
- return null;
33
+ return best?.pricing ?? null;
19
34
  }
20
35
  function calculateCost(model, inputTokens, outputTokens) {
21
36
  const pricing = resolvePricing(model);
@@ -31,14 +46,28 @@ function round6(n) {
31
46
  var openaiAdapter = {
32
47
  name: "openai",
33
48
  matches(client) {
34
- return isObject(client) && isObject(client.chat) && isObject(client.chat.completions) && typeof client.chat.completions.create === "function";
49
+ if (!isObject(client)) return false;
50
+ const chat = client.chat;
51
+ if (!isObject(chat)) return false;
52
+ const completions = chat.completions;
53
+ if (!isObject(completions)) return false;
54
+ return typeof completions.create === "function";
35
55
  },
36
56
  async call(client, options) {
57
+ if (options.stream === true) {
58
+ throw new Error(
59
+ "Weckr: stream:true is not supported by wk.chat() because token usage is not in stream responses by default. Either disable streaming, or call openai.chat.completions.create() directly outside wk.chat() (you lose cost tracking) and we will add proper streaming support in a future release."
60
+ );
61
+ }
37
62
  const { userId, feature, plan, ...rest } = options;
63
+ void userId;
64
+ void feature;
65
+ void plan;
38
66
  return client.chat.completions.create(rest);
39
67
  },
40
68
  extractUsage(result) {
41
- const usage = result?.usage ?? {};
69
+ const r = result;
70
+ const usage = r?.usage ?? {};
42
71
  return {
43
72
  inputTokens: toInt(usage.prompt_tokens ?? usage.input_tokens),
44
73
  outputTokens: toInt(usage.completion_tokens ?? usage.output_tokens)
@@ -48,14 +77,21 @@ var openaiAdapter = {
48
77
  var anthropicAdapter = {
49
78
  name: "anthropic",
50
79
  matches(client) {
51
- return isObject(client) && isObject(client.messages) && typeof client.messages.create === "function";
80
+ if (!isObject(client)) return false;
81
+ const messages = client.messages;
82
+ if (!isObject(messages)) return false;
83
+ return typeof messages.create === "function";
52
84
  },
53
85
  async call(client, options) {
54
86
  const { userId, feature, plan, ...rest } = options;
87
+ void userId;
88
+ void feature;
89
+ void plan;
55
90
  return client.messages.create(rest);
56
91
  },
57
92
  extractUsage(result) {
58
- const usage = result?.usage ?? {};
93
+ const r = result;
94
+ const usage = r?.usage ?? {};
59
95
  return {
60
96
  inputTokens: toInt(usage.input_tokens),
61
97
  outputTokens: toInt(usage.output_tokens)
@@ -65,18 +101,25 @@ var anthropicAdapter = {
65
101
  var geminiAdapter = {
66
102
  name: "gemini",
67
103
  matches(client) {
68
- return isObject(client) && isObject(client.models) && typeof client.models.generateContent === "function";
104
+ if (!isObject(client)) return false;
105
+ const models = client.models;
106
+ if (!isObject(models)) return false;
107
+ return typeof models.generateContent === "function";
69
108
  },
70
109
  async call(client, options) {
71
110
  const { userId, feature, plan, messages, ...rest } = options;
111
+ void userId;
112
+ void feature;
113
+ void plan;
72
114
  const contents = (messages ?? []).map((m) => ({
73
115
  role: m.role === "assistant" ? "model" : "user",
74
- parts: [{ text: m.content }]
116
+ parts: [{ text: typeof m.content === "string" ? m.content : JSON.stringify(m.content) }]
75
117
  }));
76
118
  return client.models.generateContent({ ...rest, contents });
77
119
  },
78
120
  extractUsage(result) {
79
- const meta = result?.usageMetadata ?? {};
121
+ const r = result;
122
+ const meta = r?.usageMetadata ?? {};
80
123
  return {
81
124
  inputTokens: toInt(meta.promptTokenCount),
82
125
  outputTokens: toInt(meta.candidatesTokenCount)
@@ -100,11 +143,15 @@ function toInt(v) {
100
143
 
101
144
  // src/logger.ts
102
145
  function createLogger(opts) {
103
- const f = opts.fetch ?? globalThis.fetch;
104
- if (typeof f !== "function") {
105
- throw new Error("Weckr: global fetch is unavailable. Pass a fetch implementation via config.fetch.");
106
- }
107
- return function log(payload) {
146
+ const inflight = /* @__PURE__ */ new Set();
147
+ function log(payload) {
148
+ const f = opts.fetch ?? globalThis.fetch;
149
+ if (typeof f !== "function") {
150
+ opts.onError?.(
151
+ new Error("Weckr: global fetch is unavailable. Pass a fetch implementation via config.fetch.")
152
+ );
153
+ return;
154
+ }
108
155
  queueMicrotask(() => {
109
156
  let promise;
110
157
  try {
@@ -121,63 +168,38 @@ function createLogger(opts) {
121
168
  opts.onError?.(err);
122
169
  return;
123
170
  }
124
- promise.then(async (res) => {
171
+ const tracked = promise.then(async (res) => {
125
172
  if (!res.ok) {
126
173
  const body = await res.text().catch(() => "");
127
174
  opts.onError?.(
128
- new Error(`Weckr log failed: ${res.status} ${res.statusText} ${body}`.trim())
175
+ new Error(
176
+ `Weckr log failed: ${res.status} ${res.statusText} ${body}. ` + (res.status === 401 || res.status === 403 ? `Verify the api key at https://app.useweckr.com/dashboard/settings.` : "")
177
+ )
129
178
  );
130
179
  }
131
180
  }).catch((err) => {
132
181
  opts.onError?.(err);
182
+ }).finally(() => {
183
+ inflight.delete(tracked);
133
184
  });
185
+ inflight.add(tracked);
134
186
  });
135
- };
136
- }
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
187
  }
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 };
188
+ async function flush(timeoutMs = 5e3) {
189
+ if (inflight.size === 0) return;
190
+ const all = Promise.allSettled(Array.from(inflight));
191
+ if (timeoutMs <= 0) {
192
+ await all;
193
+ return;
175
194
  }
176
- };
177
- }
178
- function deriveCheckEndpoint(logEndpoint) {
179
- if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
180
- return logEndpoint.replace(/\/$/, "") + "/../check";
195
+ let timer;
196
+ const timeout = new Promise((resolve) => {
197
+ timer = setTimeout(() => resolve(), timeoutMs);
198
+ });
199
+ await Promise.race([all, timeout]);
200
+ if (timer) clearTimeout(timer);
201
+ }
202
+ return { log, flush };
181
203
  }
182
204
 
183
205
  // src/errors.ts
@@ -208,15 +230,94 @@ function capCheckToError(opts) {
208
230
  cap: opts.result.cap
209
231
  });
210
232
  }
233
+ var WeckrConfigError = class extends Error {
234
+ name = "WeckrConfigError";
235
+ code;
236
+ constructor(code, message) {
237
+ super(message);
238
+ this.code = code;
239
+ }
240
+ };
241
+ function isWeckrConfigError(e) {
242
+ return e instanceof Error && e.name === "WeckrConfigError";
243
+ }
244
+
245
+ // src/cap-cache.ts
246
+ var TTL_MS = 6e4;
247
+ function cacheKey(userId, planName, model) {
248
+ return JSON.stringify([userId, planName, model ?? null]);
249
+ }
250
+ function createCapChecker(opts) {
251
+ const f = opts.fetch ?? globalThis.fetch;
252
+ const cache = /* @__PURE__ */ new Map();
253
+ const inflight = /* @__PURE__ */ new Map();
254
+ return async function checkCap(userId, planName, model) {
255
+ const k = cacheKey(userId, planName, model);
256
+ const now = Date.now();
257
+ const hit = cache.get(k);
258
+ if (hit && hit.expiresAt > now) return hit.result;
259
+ const pending = inflight.get(k);
260
+ if (pending) return pending;
261
+ if (typeof f !== "function") {
262
+ return { allowed: true };
263
+ }
264
+ const fetchPromise = (async () => {
265
+ try {
266
+ const url = new URL(opts.endpoint);
267
+ url.searchParams.set("userId", userId);
268
+ url.searchParams.set("planName", planName);
269
+ if (model) url.searchParams.set("model", model);
270
+ const res = await f(url.toString(), {
271
+ method: "GET",
272
+ headers: { "x-api-key": opts.apiKey }
273
+ });
274
+ if (res.status === 401 || res.status === 403) {
275
+ const body = await res.text().catch(() => "");
276
+ throw new WeckrConfigError(
277
+ res.status === 401 ? "invalid_api_key" : "forbidden",
278
+ `Weckr: cap-check rejected with ${res.status}. Verify the api key is correct, not revoked, and active in the dashboard. Server said: ${body || "(no body)"}`
279
+ );
280
+ }
281
+ if (!res.ok) {
282
+ opts.onError?.(
283
+ new Error(`Weckr cap check failed: ${res.status} ${res.statusText}`)
284
+ );
285
+ return { allowed: true };
286
+ }
287
+ const json = await res.json();
288
+ cache.set(k, { result: json, expiresAt: Date.now() + TTL_MS });
289
+ return json;
290
+ } catch (err) {
291
+ if (err instanceof WeckrConfigError) throw err;
292
+ opts.onError?.(err);
293
+ return { allowed: true };
294
+ } finally {
295
+ inflight.delete(k);
296
+ }
297
+ })();
298
+ inflight.set(k, fetchPromise);
299
+ return fetchPromise;
300
+ };
301
+ }
302
+ function deriveCheckEndpoint(logEndpoint) {
303
+ if (logEndpoint.endsWith("/log")) return logEndpoint.slice(0, -"/log".length) + "/check";
304
+ throw new WeckrConfigError(
305
+ "invalid_api_key",
306
+ // closest reusable code; semantically a config bug
307
+ `Weckr: cannot derive checkEndpoint from endpoint "${logEndpoint}" \u2014 it does not end in "/log". Pass an explicit \`checkEndpoint\` in the Weckr config.`
308
+ );
309
+ }
211
310
 
212
311
  // src/weckr.ts
213
- var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
312
+ var DEFAULT_ENDPOINT = "https://app.useweckr.com/api/v1/log";
214
313
  var Weckr = class {
215
314
  apiKey;
216
315
  plans;
217
- log;
316
+ logger;
218
317
  checkCap;
219
318
  onError;
319
+ onDowngrade;
320
+ downgradeSeen = /* @__PURE__ */ new Set();
220
321
  constructor(config) {
221
322
  if (!config?.apiKey) {
222
323
  throw new Error("Weckr: apiKey is required.");
@@ -224,8 +325,9 @@ var Weckr = class {
224
325
  this.apiKey = config.apiKey;
225
326
  this.plans = config.plans ?? {};
226
327
  this.onError = config.onError;
328
+ this.onDowngrade = config.onDowngrade ?? defaultDowngradeWarn.bind(this);
227
329
  const logEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
228
- this.log = createLogger({
330
+ this.logger = createLogger({
229
331
  apiKey: config.apiKey,
230
332
  endpoint: logEndpoint,
231
333
  fetch: config.fetch,
@@ -234,9 +336,10 @@ var Weckr = class {
234
336
  if (config.disableCapCheck) {
235
337
  this.checkCap = null;
236
338
  } else {
339
+ const checkEndpoint = config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint);
237
340
  this.checkCap = createCapChecker({
238
341
  apiKey: config.apiKey,
239
- endpoint: config.checkEndpoint ?? deriveCheckEndpoint(logEndpoint),
342
+ endpoint: checkEndpoint,
240
343
  fetch: config.fetch,
241
344
  onError: config.onError
242
345
  });
@@ -249,16 +352,22 @@ var Weckr = class {
249
352
  "Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
250
353
  );
251
354
  }
355
+ if (options.plan != null && !Object.prototype.hasOwnProperty.call(this.plans, options.plan)) {
356
+ throw new WeckrConfigError(
357
+ "unknown_plan",
358
+ `Weckr: plan "${options.plan}" is not in the constructor's \`plans\` map. Add it as \`plans: { "${options.plan}": <monthly_usd> }\` when constructing Weckr.`
359
+ );
360
+ }
252
361
  let effectiveOptions = options;
253
362
  if (this.checkCap && options.userId && options.plan) {
254
363
  const check = await this.checkCap(options.userId, options.plan, options.model);
255
364
  if (!check.allowed) {
256
365
  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
- }
366
+ this.onDowngrade({
367
+ userId: options.userId,
368
+ from: options.model,
369
+ to: check.alternativeModel
370
+ });
262
371
  effectiveOptions = { ...options, model: check.alternativeModel };
263
372
  } else {
264
373
  throw capCheckToError({
@@ -270,19 +379,40 @@ var Weckr = class {
270
379
  }
271
380
  }
272
381
  const startedAt = nowMs();
273
- const result = await adapter.call(client, effectiveOptions);
382
+ let result;
383
+ try {
384
+ result = await adapter.call(client, effectiveOptions);
385
+ } catch (err) {
386
+ const latencyMs2 = Math.round(nowMs() - startedAt);
387
+ this.tryLog(
388
+ adapter.name,
389
+ effectiveOptions,
390
+ { inputTokens: 0, outputTokens: 0 },
391
+ latencyMs2
392
+ );
393
+ throw err;
394
+ }
274
395
  const latencyMs = Math.round(nowMs() - startedAt);
396
+ let usage = { inputTokens: 0, outputTokens: 0 };
275
397
  try {
276
- const usage = adapter.extractUsage(result);
277
- const { costUsd } = calculateCost(effectiveOptions.model, usage.inputTokens, usage.outputTokens);
278
- const planName = effectiveOptions.plan ?? null;
398
+ usage = adapter.extractUsage(result);
399
+ } catch (err) {
400
+ this.onError?.(err);
401
+ }
402
+ this.tryLog(adapter.name, effectiveOptions, usage, latencyMs);
403
+ return result;
404
+ }
405
+ tryLog(provider, options, usage, latencyMs) {
406
+ try {
407
+ const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
408
+ const planName = options.plan ?? null;
279
409
  const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
280
- const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
410
+ const marginUsd = planRevenueUsd != null ? planRevenueUsd - costUsd : null;
281
411
  const payload = {
282
- userId: effectiveOptions.userId ?? null,
283
- feature: effectiveOptions.feature ?? null,
284
- model: effectiveOptions.model,
285
- provider: adapter.name,
412
+ userId: options.userId ?? null,
413
+ feature: options.feature ?? null,
414
+ model: options.model,
415
+ provider,
286
416
  inputTokens: usage.inputTokens,
287
417
  outputTokens: usage.outputTokens,
288
418
  costUsd,
@@ -290,29 +420,47 @@ var Weckr = class {
290
420
  planName,
291
421
  planRevenueUsd,
292
422
  marginUsd,
423
+ // sent for backward-compat; server ignores
293
424
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
294
425
  };
295
- this.log(payload);
426
+ this.logger.log(payload);
296
427
  } catch (err) {
297
428
  this.onError?.(err);
298
429
  }
299
- return result;
430
+ }
431
+ /**
432
+ * Await all in-flight log POSTs. Call this before `process.exit()` /
433
+ * `Lambda return` / end of a short-lived CLI run, otherwise the daemon
434
+ * process is torn down before the POST hits the network.
435
+ */
436
+ flush(timeoutMs) {
437
+ return this.logger.flush(timeoutMs);
300
438
  }
301
439
  };
440
+ function defaultDowngradeWarn(info) {
441
+ const key = `${info.userId}:${info.from}>${info.to}`;
442
+ const seen = this.downgradeSeen;
443
+ if (seen.has(key)) return;
444
+ seen.add(key);
445
+ if (typeof console !== "undefined" && console.warn) {
446
+ console.warn(
447
+ `Weckr: downgrading ${info.userId} from ${info.from} to ${info.to} (cap reached). Subsequent downgrades for this user/model will be silent.`
448
+ );
449
+ }
450
+ }
302
451
  function nowMs() {
303
452
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
304
453
  return performance.now();
305
454
  }
306
455
  return Date.now();
307
456
  }
308
- function round2(n) {
309
- return Math.round(n * 100) / 100;
310
- }
311
457
  export {
312
458
  PRICING,
313
459
  Weckr,
314
460
  WeckrCapError,
461
+ WeckrConfigError,
315
462
  calculateCost,
316
463
  isWeckrCapError,
464
+ isWeckrConfigError,
317
465
  resolvePricing
318
466
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weckr/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "AI cost and margin intelligence for SaaS founders",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -43,25 +43,24 @@
43
43
  "url": "https://github.com/Ghiles3232/weckr"
44
44
  },
45
45
  "peerDependencies": {
46
- "openai": ">=4.0.0"
46
+ "openai": ">=4.0.0",
47
+ "@anthropic-ai/sdk": ">=0.20.0",
48
+ "@google/genai": ">=0.4.0"
47
49
  },
48
50
  "peerDependenciesMeta": {
49
- "openai": {
50
- "optional": true
51
- },
52
- "@anthropic-ai/sdk": {
53
- "optional": true
54
- },
55
- "@google/generative-ai": {
56
- "optional": true
57
- }
51
+ "openai": { "optional": true },
52
+ "@anthropic-ai/sdk": { "optional": true },
53
+ "@google/genai": { "optional": true }
58
54
  },
59
55
  "devDependencies": {
60
56
  "@types/node": "^22.10.0",
61
57
  "openai": "^4.77.0",
62
- "tsup": "^8.3.5",
58
+ "tsup": "^8.5.1",
63
59
  "tsx": "^4.19.2",
64
60
  "typescript": "^5.7.2",
65
- "vitest": "^2.1.8"
61
+ "vitest": "^4.1.8"
62
+ },
63
+ "overrides": {
64
+ "esbuild": "0.28.1"
66
65
  }
67
66
  }