@weckr/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/index.d.mts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +237 -0
- package/dist/index.mjs +207 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @weckr/sdk
|
|
2
|
+
|
|
3
|
+
AI cost and margin intelligence for SaaS founders.
|
|
4
|
+
|
|
5
|
+
See exactly which users cost more than they pay — per LLM call, zero added latency.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @weckr/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Weckr } from '@weckr/sdk';
|
|
17
|
+
import OpenAI from 'openai';
|
|
18
|
+
|
|
19
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
20
|
+
|
|
21
|
+
const wk = new Weckr({
|
|
22
|
+
apiKey: 'wk_your_key_here',
|
|
23
|
+
plans: {
|
|
24
|
+
free: 0,
|
|
25
|
+
starter: 9,
|
|
26
|
+
pro: 29,
|
|
27
|
+
business: 99,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const result = await wk.chat(openai, {
|
|
32
|
+
model: 'gpt-4o',
|
|
33
|
+
messages: [{ role: 'user', content: prompt }],
|
|
34
|
+
userId: user.id,
|
|
35
|
+
feature: 'ai-summary',
|
|
36
|
+
plan: user.plan,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The original LLM call runs unchanged and returns immediately. After it resolves, Weckr fires an async log to the Weckr API. The log call is fire-and-forget — if it fails or stalls, your request is unaffected.
|
|
41
|
+
|
|
42
|
+
## Get your API key
|
|
43
|
+
|
|
44
|
+
Sign up at [https://useweckr.com](https://useweckr.com).
|
|
45
|
+
|
|
46
|
+
## Supported providers
|
|
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`
|
|
51
|
+
|
|
52
|
+
## What gets logged
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
{
|
|
56
|
+
userId, feature, model, provider,
|
|
57
|
+
inputTokens, outputTokens,
|
|
58
|
+
costUsd, latencyMs,
|
|
59
|
+
planName, planRevenueUsd, marginUsd,
|
|
60
|
+
timestamp,
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Cost is computed from public per-token pricing. Margin is `planRevenueUsd - costUsd` (negative means you're losing money on that user).
|
|
65
|
+
|
|
66
|
+
## Dashboard
|
|
67
|
+
|
|
68
|
+
View cost and margin data at [https://useweckr.com/dashboard](https://useweckr.com/dashboard).
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
2
|
+
interface WeckrConfig {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
plans?: Record<string, number>;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
onError?: (err: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
interface ChatOptions {
|
|
10
|
+
model: string;
|
|
11
|
+
messages: Array<{
|
|
12
|
+
role: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}>;
|
|
15
|
+
userId?: string;
|
|
16
|
+
feature?: string;
|
|
17
|
+
plan?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
interface NormalizedUsage {
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
}
|
|
24
|
+
interface LogPayload {
|
|
25
|
+
userId: string | null;
|
|
26
|
+
feature: string | null;
|
|
27
|
+
model: string;
|
|
28
|
+
provider: Provider;
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
costUsd: number;
|
|
32
|
+
latencyMs: number;
|
|
33
|
+
planName: string | null;
|
|
34
|
+
planRevenueUsd: number | null;
|
|
35
|
+
marginUsd: number | null;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
interface ProviderAdapter<TClient = unknown, TResult = unknown> {
|
|
39
|
+
name: Provider;
|
|
40
|
+
matches(client: unknown): client is TClient;
|
|
41
|
+
call(client: TClient, options: ChatOptions): Promise<TResult>;
|
|
42
|
+
extractUsage(result: TResult): NormalizedUsage;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
declare class Weckr {
|
|
46
|
+
private readonly apiKey;
|
|
47
|
+
private readonly plans;
|
|
48
|
+
private readonly log;
|
|
49
|
+
constructor(config: WeckrConfig);
|
|
50
|
+
chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ModelPricing {
|
|
54
|
+
provider: Provider;
|
|
55
|
+
inputPerMillion: number;
|
|
56
|
+
outputPerMillion: number;
|
|
57
|
+
}
|
|
58
|
+
declare const PRICING: Record<string, ModelPricing>;
|
|
59
|
+
declare function resolvePricing(model: string): ModelPricing | null;
|
|
60
|
+
declare function calculateCost(model: string, inputTokens: number, outputTokens: number): {
|
|
61
|
+
costUsd: number;
|
|
62
|
+
provider: Provider | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, type WeckrConfig, calculateCost, resolvePricing };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
2
|
+
interface WeckrConfig {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
plans?: Record<string, number>;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
onError?: (err: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
interface ChatOptions {
|
|
10
|
+
model: string;
|
|
11
|
+
messages: Array<{
|
|
12
|
+
role: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}>;
|
|
15
|
+
userId?: string;
|
|
16
|
+
feature?: string;
|
|
17
|
+
plan?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
interface NormalizedUsage {
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
}
|
|
24
|
+
interface LogPayload {
|
|
25
|
+
userId: string | null;
|
|
26
|
+
feature: string | null;
|
|
27
|
+
model: string;
|
|
28
|
+
provider: Provider;
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
costUsd: number;
|
|
32
|
+
latencyMs: number;
|
|
33
|
+
planName: string | null;
|
|
34
|
+
planRevenueUsd: number | null;
|
|
35
|
+
marginUsd: number | null;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
interface ProviderAdapter<TClient = unknown, TResult = unknown> {
|
|
39
|
+
name: Provider;
|
|
40
|
+
matches(client: unknown): client is TClient;
|
|
41
|
+
call(client: TClient, options: ChatOptions): Promise<TResult>;
|
|
42
|
+
extractUsage(result: TResult): NormalizedUsage;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
declare class Weckr {
|
|
46
|
+
private readonly apiKey;
|
|
47
|
+
private readonly plans;
|
|
48
|
+
private readonly log;
|
|
49
|
+
constructor(config: WeckrConfig);
|
|
50
|
+
chat<TClient, TResult = unknown>(client: TClient, options: ChatOptions): Promise<TResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ModelPricing {
|
|
54
|
+
provider: Provider;
|
|
55
|
+
inputPerMillion: number;
|
|
56
|
+
outputPerMillion: number;
|
|
57
|
+
}
|
|
58
|
+
declare const PRICING: Record<string, ModelPricing>;
|
|
59
|
+
declare function resolvePricing(model: string): ModelPricing | null;
|
|
60
|
+
declare function calculateCost(model: string, inputTokens: number, outputTokens: number): {
|
|
61
|
+
costUsd: number;
|
|
62
|
+
provider: Provider | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { type ChatOptions, type LogPayload, type NormalizedUsage, PRICING, type Provider, type ProviderAdapter, Weckr, type WeckrConfig, calculateCost, resolvePricing };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PRICING: () => PRICING,
|
|
24
|
+
Weckr: () => Weckr,
|
|
25
|
+
calculateCost: () => calculateCost,
|
|
26
|
+
resolvePricing: () => resolvePricing
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/pricing.ts
|
|
31
|
+
var PRICING = {
|
|
32
|
+
"gpt-4o": { provider: "openai", inputPerMillion: 2.5, outputPerMillion: 10 },
|
|
33
|
+
"gpt-4o-mini": { provider: "openai", inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
34
|
+
"gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
|
|
35
|
+
"claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
|
|
36
|
+
"claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
|
|
37
|
+
"claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
38
|
+
"gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
39
|
+
"gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
|
|
40
|
+
};
|
|
41
|
+
function resolvePricing(model) {
|
|
42
|
+
if (PRICING[model]) return PRICING[model];
|
|
43
|
+
const lower = model.toLowerCase();
|
|
44
|
+
for (const key of Object.keys(PRICING)) {
|
|
45
|
+
if (lower.startsWith(key.toLowerCase())) return PRICING[key];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
50
|
+
const pricing = resolvePricing(model);
|
|
51
|
+
if (!pricing) return { costUsd: 0, provider: null };
|
|
52
|
+
const cost = inputTokens / 1e6 * pricing.inputPerMillion + outputTokens / 1e6 * pricing.outputPerMillion;
|
|
53
|
+
return { costUsd: round6(cost), provider: pricing.provider };
|
|
54
|
+
}
|
|
55
|
+
function round6(n) {
|
|
56
|
+
return Math.round(n * 1e6) / 1e6;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/providers.ts
|
|
60
|
+
var openaiAdapter = {
|
|
61
|
+
name: "openai",
|
|
62
|
+
matches(client) {
|
|
63
|
+
return isObject(client) && isObject(client.chat) && isObject(client.chat.completions) && typeof client.chat.completions.create === "function";
|
|
64
|
+
},
|
|
65
|
+
async call(client, options) {
|
|
66
|
+
const { userId, feature, plan, ...rest } = options;
|
|
67
|
+
return client.chat.completions.create(rest);
|
|
68
|
+
},
|
|
69
|
+
extractUsage(result) {
|
|
70
|
+
const usage = result?.usage ?? {};
|
|
71
|
+
return {
|
|
72
|
+
inputTokens: toInt(usage.prompt_tokens ?? usage.input_tokens),
|
|
73
|
+
outputTokens: toInt(usage.completion_tokens ?? usage.output_tokens)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var anthropicAdapter = {
|
|
78
|
+
name: "anthropic",
|
|
79
|
+
matches(client) {
|
|
80
|
+
return isObject(client) && isObject(client.messages) && typeof client.messages.create === "function";
|
|
81
|
+
},
|
|
82
|
+
async call(client, options) {
|
|
83
|
+
const { userId, feature, plan, ...rest } = options;
|
|
84
|
+
return client.messages.create(rest);
|
|
85
|
+
},
|
|
86
|
+
extractUsage(result) {
|
|
87
|
+
const usage = result?.usage ?? {};
|
|
88
|
+
return {
|
|
89
|
+
inputTokens: toInt(usage.input_tokens),
|
|
90
|
+
outputTokens: toInt(usage.output_tokens)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var geminiAdapter = {
|
|
95
|
+
name: "gemini",
|
|
96
|
+
matches(client) {
|
|
97
|
+
return isObject(client) && isObject(client.models) && typeof client.models.generateContent === "function";
|
|
98
|
+
},
|
|
99
|
+
async call(client, options) {
|
|
100
|
+
const { userId, feature, plan, messages, ...rest } = options;
|
|
101
|
+
const contents = (messages ?? []).map((m) => ({
|
|
102
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
103
|
+
parts: [{ text: m.content }]
|
|
104
|
+
}));
|
|
105
|
+
return client.models.generateContent({ ...rest, contents });
|
|
106
|
+
},
|
|
107
|
+
extractUsage(result) {
|
|
108
|
+
const meta = result?.usageMetadata ?? {};
|
|
109
|
+
return {
|
|
110
|
+
inputTokens: toInt(meta.promptTokenCount),
|
|
111
|
+
outputTokens: toInt(meta.candidatesTokenCount)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var adapters = [openaiAdapter, anthropicAdapter, geminiAdapter];
|
|
116
|
+
function detectAdapter(client) {
|
|
117
|
+
for (const adapter of adapters) {
|
|
118
|
+
if (adapter.matches(client)) return adapter;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function isObject(v) {
|
|
123
|
+
return typeof v === "object" && v !== null;
|
|
124
|
+
}
|
|
125
|
+
function toInt(v) {
|
|
126
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
127
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/logger.ts
|
|
131
|
+
function createLogger(opts) {
|
|
132
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
133
|
+
if (typeof f !== "function") {
|
|
134
|
+
throw new Error("Weckr: global fetch is unavailable. Pass a fetch implementation via config.fetch.");
|
|
135
|
+
}
|
|
136
|
+
return function log(payload) {
|
|
137
|
+
queueMicrotask(() => {
|
|
138
|
+
let promise;
|
|
139
|
+
try {
|
|
140
|
+
promise = f(opts.endpoint, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
"x-api-key": opts.apiKey
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(payload),
|
|
147
|
+
keepalive: true
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
opts.onError?.(err);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
promise.then(async (res) => {
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const body = await res.text().catch(() => "");
|
|
156
|
+
opts.onError?.(
|
|
157
|
+
new Error(`Weckr log failed: ${res.status} ${res.statusText} ${body}`.trim())
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}).catch((err) => {
|
|
161
|
+
opts.onError?.(err);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/weckr.ts
|
|
168
|
+
var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
|
|
169
|
+
var Weckr = class {
|
|
170
|
+
apiKey;
|
|
171
|
+
plans;
|
|
172
|
+
log;
|
|
173
|
+
constructor(config) {
|
|
174
|
+
if (!config?.apiKey) {
|
|
175
|
+
throw new Error("Weckr: apiKey is required.");
|
|
176
|
+
}
|
|
177
|
+
this.apiKey = config.apiKey;
|
|
178
|
+
this.plans = config.plans ?? {};
|
|
179
|
+
this.log = createLogger({
|
|
180
|
+
apiKey: config.apiKey,
|
|
181
|
+
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
|
182
|
+
fetch: config.fetch,
|
|
183
|
+
onError: config.onError
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async chat(client, options) {
|
|
187
|
+
const adapter = detectAdapter(client);
|
|
188
|
+
if (!adapter) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const startedAt = nowMs();
|
|
194
|
+
const result = await adapter.call(client, options);
|
|
195
|
+
const latencyMs = Math.round(nowMs() - startedAt);
|
|
196
|
+
try {
|
|
197
|
+
const usage = adapter.extractUsage(result);
|
|
198
|
+
const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
|
|
199
|
+
const planName = options.plan ?? null;
|
|
200
|
+
const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
|
|
201
|
+
const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
|
|
202
|
+
const payload = {
|
|
203
|
+
userId: options.userId ?? null,
|
|
204
|
+
feature: options.feature ?? null,
|
|
205
|
+
model: options.model,
|
|
206
|
+
provider: adapter.name,
|
|
207
|
+
inputTokens: usage.inputTokens,
|
|
208
|
+
outputTokens: usage.outputTokens,
|
|
209
|
+
costUsd,
|
|
210
|
+
latencyMs,
|
|
211
|
+
planName,
|
|
212
|
+
planRevenueUsd,
|
|
213
|
+
marginUsd,
|
|
214
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
215
|
+
};
|
|
216
|
+
this.log(payload);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
function nowMs() {
|
|
223
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
224
|
+
return performance.now();
|
|
225
|
+
}
|
|
226
|
+
return Date.now();
|
|
227
|
+
}
|
|
228
|
+
function round2(n) {
|
|
229
|
+
return Math.round(n * 100) / 100;
|
|
230
|
+
}
|
|
231
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
232
|
+
0 && (module.exports = {
|
|
233
|
+
PRICING,
|
|
234
|
+
Weckr,
|
|
235
|
+
calculateCost,
|
|
236
|
+
resolvePricing
|
|
237
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/pricing.ts
|
|
2
|
+
var PRICING = {
|
|
3
|
+
"gpt-4o": { provider: "openai", inputPerMillion: 2.5, outputPerMillion: 10 },
|
|
4
|
+
"gpt-4o-mini": { provider: "openai", inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
5
|
+
"gpt-3.5-turbo": { provider: "openai", inputPerMillion: 0.5, outputPerMillion: 1.5 },
|
|
6
|
+
"claude-opus-4": { provider: "anthropic", inputPerMillion: 15, outputPerMillion: 75 },
|
|
7
|
+
"claude-sonnet-4": { provider: "anthropic", inputPerMillion: 3, outputPerMillion: 15 },
|
|
8
|
+
"claude-haiku-4": { provider: "anthropic", inputPerMillion: 0.8, outputPerMillion: 4 },
|
|
9
|
+
"gemini-2.5-flash": { provider: "gemini", inputPerMillion: 0.15, outputPerMillion: 0.6 },
|
|
10
|
+
"gemini-2.5-pro": { provider: "gemini", inputPerMillion: 1.25, outputPerMillion: 10 }
|
|
11
|
+
};
|
|
12
|
+
function resolvePricing(model) {
|
|
13
|
+
if (PRICING[model]) return PRICING[model];
|
|
14
|
+
const lower = model.toLowerCase();
|
|
15
|
+
for (const key of Object.keys(PRICING)) {
|
|
16
|
+
if (lower.startsWith(key.toLowerCase())) return PRICING[key];
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
21
|
+
const pricing = resolvePricing(model);
|
|
22
|
+
if (!pricing) return { costUsd: 0, provider: null };
|
|
23
|
+
const cost = inputTokens / 1e6 * pricing.inputPerMillion + outputTokens / 1e6 * pricing.outputPerMillion;
|
|
24
|
+
return { costUsd: round6(cost), provider: pricing.provider };
|
|
25
|
+
}
|
|
26
|
+
function round6(n) {
|
|
27
|
+
return Math.round(n * 1e6) / 1e6;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/providers.ts
|
|
31
|
+
var openaiAdapter = {
|
|
32
|
+
name: "openai",
|
|
33
|
+
matches(client) {
|
|
34
|
+
return isObject(client) && isObject(client.chat) && isObject(client.chat.completions) && typeof client.chat.completions.create === "function";
|
|
35
|
+
},
|
|
36
|
+
async call(client, options) {
|
|
37
|
+
const { userId, feature, plan, ...rest } = options;
|
|
38
|
+
return client.chat.completions.create(rest);
|
|
39
|
+
},
|
|
40
|
+
extractUsage(result) {
|
|
41
|
+
const usage = result?.usage ?? {};
|
|
42
|
+
return {
|
|
43
|
+
inputTokens: toInt(usage.prompt_tokens ?? usage.input_tokens),
|
|
44
|
+
outputTokens: toInt(usage.completion_tokens ?? usage.output_tokens)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var anthropicAdapter = {
|
|
49
|
+
name: "anthropic",
|
|
50
|
+
matches(client) {
|
|
51
|
+
return isObject(client) && isObject(client.messages) && typeof client.messages.create === "function";
|
|
52
|
+
},
|
|
53
|
+
async call(client, options) {
|
|
54
|
+
const { userId, feature, plan, ...rest } = options;
|
|
55
|
+
return client.messages.create(rest);
|
|
56
|
+
},
|
|
57
|
+
extractUsage(result) {
|
|
58
|
+
const usage = result?.usage ?? {};
|
|
59
|
+
return {
|
|
60
|
+
inputTokens: toInt(usage.input_tokens),
|
|
61
|
+
outputTokens: toInt(usage.output_tokens)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var geminiAdapter = {
|
|
66
|
+
name: "gemini",
|
|
67
|
+
matches(client) {
|
|
68
|
+
return isObject(client) && isObject(client.models) && typeof client.models.generateContent === "function";
|
|
69
|
+
},
|
|
70
|
+
async call(client, options) {
|
|
71
|
+
const { userId, feature, plan, messages, ...rest } = options;
|
|
72
|
+
const contents = (messages ?? []).map((m) => ({
|
|
73
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
74
|
+
parts: [{ text: m.content }]
|
|
75
|
+
}));
|
|
76
|
+
return client.models.generateContent({ ...rest, contents });
|
|
77
|
+
},
|
|
78
|
+
extractUsage(result) {
|
|
79
|
+
const meta = result?.usageMetadata ?? {};
|
|
80
|
+
return {
|
|
81
|
+
inputTokens: toInt(meta.promptTokenCount),
|
|
82
|
+
outputTokens: toInt(meta.candidatesTokenCount)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var adapters = [openaiAdapter, anthropicAdapter, geminiAdapter];
|
|
87
|
+
function detectAdapter(client) {
|
|
88
|
+
for (const adapter of adapters) {
|
|
89
|
+
if (adapter.matches(client)) return adapter;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function isObject(v) {
|
|
94
|
+
return typeof v === "object" && v !== null;
|
|
95
|
+
}
|
|
96
|
+
function toInt(v) {
|
|
97
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
98
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/logger.ts
|
|
102
|
+
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) {
|
|
108
|
+
queueMicrotask(() => {
|
|
109
|
+
let promise;
|
|
110
|
+
try {
|
|
111
|
+
promise = f(opts.endpoint, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"content-type": "application/json",
|
|
115
|
+
"x-api-key": opts.apiKey
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(payload),
|
|
118
|
+
keepalive: true
|
|
119
|
+
});
|
|
120
|
+
} catch (err) {
|
|
121
|
+
opts.onError?.(err);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
promise.then(async (res) => {
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const body = await res.text().catch(() => "");
|
|
127
|
+
opts.onError?.(
|
|
128
|
+
new Error(`Weckr log failed: ${res.status} ${res.statusText} ${body}`.trim())
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}).catch((err) => {
|
|
132
|
+
opts.onError?.(err);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/weckr.ts
|
|
139
|
+
var DEFAULT_ENDPOINT = "https://useweckr.com/api/v1/log";
|
|
140
|
+
var Weckr = class {
|
|
141
|
+
apiKey;
|
|
142
|
+
plans;
|
|
143
|
+
log;
|
|
144
|
+
constructor(config) {
|
|
145
|
+
if (!config?.apiKey) {
|
|
146
|
+
throw new Error("Weckr: apiKey is required.");
|
|
147
|
+
}
|
|
148
|
+
this.apiKey = config.apiKey;
|
|
149
|
+
this.plans = config.plans ?? {};
|
|
150
|
+
this.log = createLogger({
|
|
151
|
+
apiKey: config.apiKey,
|
|
152
|
+
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
|
153
|
+
fetch: config.fetch,
|
|
154
|
+
onError: config.onError
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async chat(client, options) {
|
|
158
|
+
const adapter = detectAdapter(client);
|
|
159
|
+
if (!adapter) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Weckr: could not detect provider. Pass an OpenAI, Anthropic, or Gemini client instance."
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const startedAt = nowMs();
|
|
165
|
+
const result = await adapter.call(client, options);
|
|
166
|
+
const latencyMs = Math.round(nowMs() - startedAt);
|
|
167
|
+
try {
|
|
168
|
+
const usage = adapter.extractUsage(result);
|
|
169
|
+
const { costUsd } = calculateCost(options.model, usage.inputTokens, usage.outputTokens);
|
|
170
|
+
const planName = options.plan ?? null;
|
|
171
|
+
const planRevenueUsd = planName != null && Object.prototype.hasOwnProperty.call(this.plans, planName) ? this.plans[planName] : null;
|
|
172
|
+
const marginUsd = planRevenueUsd != null ? round2(planRevenueUsd - costUsd) : null;
|
|
173
|
+
const payload = {
|
|
174
|
+
userId: options.userId ?? null,
|
|
175
|
+
feature: options.feature ?? null,
|
|
176
|
+
model: options.model,
|
|
177
|
+
provider: adapter.name,
|
|
178
|
+
inputTokens: usage.inputTokens,
|
|
179
|
+
outputTokens: usage.outputTokens,
|
|
180
|
+
costUsd,
|
|
181
|
+
latencyMs,
|
|
182
|
+
planName,
|
|
183
|
+
planRevenueUsd,
|
|
184
|
+
marginUsd,
|
|
185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
186
|
+
};
|
|
187
|
+
this.log(payload);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function nowMs() {
|
|
194
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
195
|
+
return performance.now();
|
|
196
|
+
}
|
|
197
|
+
return Date.now();
|
|
198
|
+
}
|
|
199
|
+
function round2(n) {
|
|
200
|
+
return Math.round(n * 100) / 100;
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
PRICING,
|
|
204
|
+
Weckr,
|
|
205
|
+
calculateCost,
|
|
206
|
+
resolvePricing
|
|
207
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weckr/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI cost and margin intelligence for SaaS founders",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"ai",
|
|
29
|
+
"llm",
|
|
30
|
+
"openai",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"gemini",
|
|
33
|
+
"cost",
|
|
34
|
+
"monitoring",
|
|
35
|
+
"saas",
|
|
36
|
+
"finops"
|
|
37
|
+
],
|
|
38
|
+
"author": "Weckr",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"homepage": "https://useweckr.com",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/Ghiles3232/weckr"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"openai": ">=4.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"openai": { "optional": true },
|
|
50
|
+
"@anthropic-ai/sdk": { "optional": true },
|
|
51
|
+
"@google/generative-ai": { "optional": true }
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.10.0",
|
|
55
|
+
"openai": "^4.77.0",
|
|
56
|
+
"tsup": "^8.3.5",
|
|
57
|
+
"tsx": "^4.19.2",
|
|
58
|
+
"typescript": "^5.7.2",
|
|
59
|
+
"vitest": "^2.1.8"
|
|
60
|
+
}
|
|
61
|
+
}
|