@aipricinglab/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 +231 -0
- package/dist/index.d.mts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +141 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @aipricinglab/sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@aipricinglab/sdk)
|
|
4
|
+
|
|
5
|
+
The official TypeScript SDK for **[AiPricingLab](https://aipricinglab.space)** — drop-in usage metering, limits, and plan enforcement for AI-powered apps.
|
|
6
|
+
|
|
7
|
+
Stop building your own meter for LLM tokens, image generations, video seconds, or any other AI consumption. Define plans in the dashboard, install this SDK, and ship.
|
|
8
|
+
|
|
9
|
+
- 🌐 Website: <https://aipricinglab.space>
|
|
10
|
+
- 📚 Docs: <https://aipricinglab.space/docs>
|
|
11
|
+
- 🐛 Issues: <https://aipricinglab.space/support>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Provider-agnostic** — works with OpenAI, Anthropic, Replicate, Fal, your own models, anything you bill against.
|
|
18
|
+
- **Atomic reservations** — `reserve` / `commit` / `release` prevents parallel requests from blowing past limits.
|
|
19
|
+
- **Zero runtime dependencies** — uses the platform `fetch`. Tiny bundle, works in Node, Bun, Deno, Edge, and the browser.
|
|
20
|
+
- **Typed errors** — every failure mode is a typed `AiPricingLabError` with a stable `code`.
|
|
21
|
+
- **Public + secret keys** — read a user's own usage from the browser with `pk_live_...`, do everything else from the backend with `sk_live_...`.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @aipricinglab/sdk
|
|
27
|
+
# or
|
|
28
|
+
pnpm add @aipricinglab/sdk
|
|
29
|
+
# or
|
|
30
|
+
yarn add @aipricinglab/sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Node 18+ (or any runtime with global `fetch`).
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Grab a secret key from your app's **API keys** tab at <https://aipricinglab.space>, then:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createClient } from '@aipricinglab/sdk';
|
|
41
|
+
|
|
42
|
+
const apl = createClient({ apiKey: process.env.AIPRICINGLAB_KEY! });
|
|
43
|
+
|
|
44
|
+
// Just record consumption — fire-and-forget.
|
|
45
|
+
await apl.track('user_abc123', 'image.flux-pro', 1);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it. The event is now visible in your AiPricingLab dashboard, counted against any matching plan limits.
|
|
49
|
+
|
|
50
|
+
### Event naming convention
|
|
51
|
+
|
|
52
|
+
The plan builder generates match rules in the form **`<category>.<modelId>`**, where `category` is one of `image`, `llm`, `video`, `audio`. Use the same shape in your `track` / `canUse` / `reserve` calls so events count toward plan rows automatically:
|
|
53
|
+
|
|
54
|
+
| Category | Example event name |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Image generation | `image.flux-pro`, `image.gemini-3.1-image` |
|
|
57
|
+
| Language model | `llm.gpt-4o`, `llm.claude-sonnet-4-6` |
|
|
58
|
+
| Video generation | `video.runway-gen3`, `video.veo-2` |
|
|
59
|
+
| Audio generation | `audio.elevenlabs-v2`, `audio.whisper` |
|
|
60
|
+
|
|
61
|
+
A plan can also have a **category total** (e.g. *"30 images of any kind per month"*). Those use the wildcard pattern `<category>.*`, which matches any event that starts with that prefix — you don't need to emit anything special.
|
|
62
|
+
|
|
63
|
+
You're free to use any other event names too — they just won't match the structural plan rows; they'll only fire if you've added a matching custom group.
|
|
64
|
+
|
|
65
|
+
## Reserve / commit / release (recommended)
|
|
66
|
+
|
|
67
|
+
For strict enforcement, use the three-step pattern. It atomically increments the counter *before* the AI call so two parallel requests can't both squeeze through under the limit:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createClient, AiPricingLabError } from '@aipricinglab/sdk';
|
|
71
|
+
|
|
72
|
+
const apl = createClient({ apiKey: process.env.AIPRICINGLAB_KEY! });
|
|
73
|
+
|
|
74
|
+
async function generateImage(userId: string, prompt: string) {
|
|
75
|
+
// 1. Reserve quota before the AI call.
|
|
76
|
+
const reservation = await apl.reserve(userId, 'image.flux-pro', 1);
|
|
77
|
+
|
|
78
|
+
if (!reservation.allowed) {
|
|
79
|
+
throw new Error(`Limit reached: ${reservation.reasons?.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const image = await callFluxPro(prompt);
|
|
84
|
+
|
|
85
|
+
// 2. Commit on success — the charge is final.
|
|
86
|
+
await apl.commit(reservation.reservationId!);
|
|
87
|
+
return image;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// 3. Release on failure — the user is not charged.
|
|
90
|
+
await apl.release(reservation.reservationId!);
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Reservations auto-expire after 60 seconds if you forget to commit or release, so a crashed server won't permanently lock quota.
|
|
97
|
+
|
|
98
|
+
## Track (fire-and-forget)
|
|
99
|
+
|
|
100
|
+
Simpler, no atomicity guarantee. Use it when slight over-counting is acceptable.
|
|
101
|
+
|
|
102
|
+
### `quantity` semantics
|
|
103
|
+
|
|
104
|
+
`quantity` is **the number of units the event consumed** in whatever unit the matching plan limit uses:
|
|
105
|
+
|
|
106
|
+
- For an `image` plan with unit `count` → `quantity = 1` per generation.
|
|
107
|
+
- For an `llm` plan with unit `tokens` → `quantity = totalInputTokens + totalOutputTokens`.
|
|
108
|
+
- For an `llm` plan with unit `cents` → `quantity = costInCents` (integer cents).
|
|
109
|
+
- For a `video` plan with unit `seconds` → `quantity = clipLengthInSeconds`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// LLM call — pass total tokens consumed
|
|
113
|
+
await apl.track('user_abc123', 'llm.gpt-4o', 1842, {
|
|
114
|
+
inputTokens: '920',
|
|
115
|
+
outputTokens: '922',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Video clip — pass duration in seconds
|
|
119
|
+
await apl.track('user_abc123', 'video.runway-gen3', 8);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Check without charging
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const { allowed, reasons } = await apl.canUse('user_abc123', 'image.flux-pro');
|
|
126
|
+
|
|
127
|
+
if (!allowed) {
|
|
128
|
+
return showUpgradePrompt(reasons);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or the boolean shorthand:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
if (await apl.can('user_abc123', 'image.flux-pro')) {
|
|
136
|
+
// proceed
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Variant-gated plan rows
|
|
141
|
+
|
|
142
|
+
Some plans gate a model by output tier — e.g. *"100 standard images, 10 4K images"*. Pass the `variant` key in `metadata` so the right bucket is incremented:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
await apl.track('user_abc123', 'image.gemini-3.1-image', 1, { variant: '4k' });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`variant` is the only reserved metadata key — everything else is yours.
|
|
149
|
+
|
|
150
|
+
### Metadata is string-only
|
|
151
|
+
|
|
152
|
+
Metadata values must be strings (`Record<string, string>`). The API rejects numbers and booleans, so coerce them on your side:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
await apl.track('user_abc123', 'llm.gpt-4o', 1842, {
|
|
156
|
+
inputTokens: String(920), // ✅
|
|
157
|
+
cached: String(true), // ✅
|
|
158
|
+
// outputTokens: 922, // ❌ rejected
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Read a user's own usage (browser-safe)
|
|
163
|
+
|
|
164
|
+
Use a `pk_live_...` key in client-side code. Public keys can only read usage; they cannot track, reserve, or modify subscriptions. The SDK auto-routes public keys to the public-safe endpoint:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const apl = createClient({ apiKey: 'pk_live_...' });
|
|
168
|
+
|
|
169
|
+
const usage = await apl.usage(currentUserId);
|
|
170
|
+
// → { userId, period: { start, end } | null, counters: [{ groupId, count, costCents }, ...] }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`period` is `null` when the user has no active subscription on the app.
|
|
174
|
+
|
|
175
|
+
## Manage subscriptions
|
|
176
|
+
|
|
177
|
+
Move a user onto a plan (or off it) from your backend:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
await apl.upsertSubscription({
|
|
181
|
+
userId: 'user_abc123',
|
|
182
|
+
planId: 'plan_pro_monthly',
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Error handling
|
|
187
|
+
|
|
188
|
+
Every method throws a typed `AiPricingLabError` on failure:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { AiPricingLabError } from '@aipricinglab/sdk';
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await apl.track('user_abc123', 'image.flux-pro');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof AiPricingLabError) {
|
|
197
|
+
console.error(err.code, err.status, err.message);
|
|
198
|
+
// err.code: 'limit_reached' | 'invalid_key' | 'not_found' | 'workspace_limit_reached' | ...
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## API reference
|
|
204
|
+
|
|
205
|
+
| Method | Purpose |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `track(userId, event, quantity?, metadata?)` | Record consumption. |
|
|
208
|
+
| `canUse(userId, event, quantity?, metadata?)` | Check if allowed; returns `{ allowed, reasons, details }`. |
|
|
209
|
+
| `can(userId, event, quantity?, metadata?)` | Boolean shorthand for `canUse`. |
|
|
210
|
+
| `reserve(userId, event, quantity?, metadata?)` | Atomically hold quota; returns a `reservationId`. |
|
|
211
|
+
| `commit(reservationId)` | Confirm a reservation. |
|
|
212
|
+
| `release(reservationId)` | Cancel a reservation; quota returned. |
|
|
213
|
+
| `usage(userId, event?)` | Read a user's current counters. |
|
|
214
|
+
| `upsertSubscription({ userId, planId })` | Assign or change a user's plan. |
|
|
215
|
+
|
|
216
|
+
Full request/response types are exported from the package — your editor will pick them up.
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
createClient({
|
|
222
|
+
apiKey: 'sk_live_...', // required
|
|
223
|
+
baseUrl: 'https://www.aipricinglab.space', // optional, defaults to production
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Set `baseUrl` only if you're running a self-hosted instance.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT © [AiPricingLab](https://aipricinglab.space)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
type LimitUnit = 'count' | 'tokens' | 'seconds' | 'cents';
|
|
2
|
+
interface MatchRule {
|
|
3
|
+
event: string;
|
|
4
|
+
metadata?: Record<string, string> | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface LimitGroup {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
unit: LimitUnit;
|
|
10
|
+
quota: number;
|
|
11
|
+
matches: MatchRule[];
|
|
12
|
+
}
|
|
13
|
+
interface PlanLimits {
|
|
14
|
+
groups: LimitGroup[];
|
|
15
|
+
}
|
|
16
|
+
type PeriodType = 'daily' | 'weekly' | 'monthly' | 'lifetime';
|
|
17
|
+
type PeriodAnchor = 'subscription_start' | 'calendar';
|
|
18
|
+
type EventMetadata = Record<string, string>;
|
|
19
|
+
interface TrackResponseData {
|
|
20
|
+
eventId: string;
|
|
21
|
+
counters: CounterSummary[];
|
|
22
|
+
}
|
|
23
|
+
interface CounterSummary {
|
|
24
|
+
groupId: string;
|
|
25
|
+
count: number;
|
|
26
|
+
costCents: number;
|
|
27
|
+
}
|
|
28
|
+
type ErrorCode = 'not_found' | 'invalid_key' | 'requires_secret_key' | 'limit_reached' | 'workspace_limit_reached' | 'invalid_request' | 'reservation_expired' | 'reservation_not_pending' | 'internal_error' | 'not_implemented';
|
|
29
|
+
interface ApiError {
|
|
30
|
+
code: ErrorCode;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
interface ApiOk<T> {
|
|
34
|
+
ok: true;
|
|
35
|
+
data: T;
|
|
36
|
+
}
|
|
37
|
+
interface ApiFail {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: ApiError;
|
|
40
|
+
}
|
|
41
|
+
type ApiResponse<T> = ApiOk<T> | ApiFail;
|
|
42
|
+
interface CanUseResponseData {
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
reasons: string[];
|
|
45
|
+
details: {
|
|
46
|
+
groupId: string;
|
|
47
|
+
current: number;
|
|
48
|
+
quota: number;
|
|
49
|
+
resetsAt: string | null;
|
|
50
|
+
}[];
|
|
51
|
+
}
|
|
52
|
+
interface ReserveResponseData {
|
|
53
|
+
allowed: boolean;
|
|
54
|
+
reservationId?: string;
|
|
55
|
+
expiresAt?: string;
|
|
56
|
+
reasons?: string[];
|
|
57
|
+
}
|
|
58
|
+
interface UsageResponseData {
|
|
59
|
+
userId: string;
|
|
60
|
+
period: {
|
|
61
|
+
start: string;
|
|
62
|
+
end: string | null;
|
|
63
|
+
} | null;
|
|
64
|
+
counters: CounterSummary[];
|
|
65
|
+
}
|
|
66
|
+
interface UpsertSubscriptionRequest {
|
|
67
|
+
userId: string;
|
|
68
|
+
planId: string;
|
|
69
|
+
customLimits?: PlanLimits;
|
|
70
|
+
endsAt?: string;
|
|
71
|
+
}
|
|
72
|
+
interface UpsertSubscriptionResponseData {
|
|
73
|
+
subscriptionId: string;
|
|
74
|
+
userId: string;
|
|
75
|
+
planId: string;
|
|
76
|
+
startedAt: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ClientOptions {
|
|
80
|
+
apiKey: string;
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
declare class AiPricingLabClient {
|
|
84
|
+
private readonly apiKey;
|
|
85
|
+
private readonly baseUrl;
|
|
86
|
+
constructor(opts: ClientOptions);
|
|
87
|
+
/**
|
|
88
|
+
* Record that a user consumed something.
|
|
89
|
+
*
|
|
90
|
+
* `metadata` is matched against the `metadata` filters on plan match rules.
|
|
91
|
+
* The dashboard uses one reserved key — **`variant`** — when a plan row gates
|
|
92
|
+
* a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,
|
|
93
|
+
* video tier `standard`/`fast`/`lite`). Pass it through so variant-gated
|
|
94
|
+
* buckets count the event:
|
|
95
|
+
*
|
|
96
|
+
* await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });
|
|
97
|
+
*/
|
|
98
|
+
track(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<TrackResponseData>;
|
|
99
|
+
/**
|
|
100
|
+
* Check if a user is allowed to perform an action.
|
|
101
|
+
* Returns `{ allowed, reasons, details }`.
|
|
102
|
+
* Use `.can()` if you only need the boolean.
|
|
103
|
+
*
|
|
104
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
105
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
106
|
+
*/
|
|
107
|
+
canUse(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<CanUseResponseData>;
|
|
108
|
+
/** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */
|
|
109
|
+
can(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* Atomically reserve quota before doing the AI call.
|
|
112
|
+
* On success call `commit(reservationId)`, on failure call `release(reservationId)`.
|
|
113
|
+
*
|
|
114
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
115
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
116
|
+
*/
|
|
117
|
+
reserve(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<ReserveResponseData>;
|
|
118
|
+
/** Confirm a reservation after a successful AI call. */
|
|
119
|
+
commit(reservationId: string): Promise<void>;
|
|
120
|
+
/** Cancel a reservation after a failed AI call so quota is returned. */
|
|
121
|
+
release(reservationId: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Get current usage counters for a user.
|
|
124
|
+
*
|
|
125
|
+
* Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint
|
|
126
|
+
* is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.
|
|
127
|
+
*/
|
|
128
|
+
usage(userId: string, event?: string): Promise<UsageResponseData>;
|
|
129
|
+
/** Assign or update a user's plan subscription. */
|
|
130
|
+
upsertSubscription(params: UpsertSubscriptionRequest): Promise<UpsertSubscriptionResponseData>;
|
|
131
|
+
}
|
|
132
|
+
declare function createClient(opts: ClientOptions): AiPricingLabClient;
|
|
133
|
+
|
|
134
|
+
declare class AiPricingLabError extends Error {
|
|
135
|
+
readonly code: ErrorCode;
|
|
136
|
+
readonly status: number;
|
|
137
|
+
constructor(opts: {
|
|
138
|
+
code: ErrorCode;
|
|
139
|
+
message: string;
|
|
140
|
+
status: number;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { AiPricingLabClient, AiPricingLabError, type ApiError, type ApiResponse, type CanUseResponseData, type ClientOptions, type CounterSummary, type ErrorCode, type EventMetadata, type LimitGroup, type LimitUnit, type MatchRule, type PeriodAnchor, type PeriodType, type PlanLimits, type ReserveResponseData, type TrackResponseData, type UpsertSubscriptionRequest, type UpsertSubscriptionResponseData, type UsageResponseData, createClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
type LimitUnit = 'count' | 'tokens' | 'seconds' | 'cents';
|
|
2
|
+
interface MatchRule {
|
|
3
|
+
event: string;
|
|
4
|
+
metadata?: Record<string, string> | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface LimitGroup {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
unit: LimitUnit;
|
|
10
|
+
quota: number;
|
|
11
|
+
matches: MatchRule[];
|
|
12
|
+
}
|
|
13
|
+
interface PlanLimits {
|
|
14
|
+
groups: LimitGroup[];
|
|
15
|
+
}
|
|
16
|
+
type PeriodType = 'daily' | 'weekly' | 'monthly' | 'lifetime';
|
|
17
|
+
type PeriodAnchor = 'subscription_start' | 'calendar';
|
|
18
|
+
type EventMetadata = Record<string, string>;
|
|
19
|
+
interface TrackResponseData {
|
|
20
|
+
eventId: string;
|
|
21
|
+
counters: CounterSummary[];
|
|
22
|
+
}
|
|
23
|
+
interface CounterSummary {
|
|
24
|
+
groupId: string;
|
|
25
|
+
count: number;
|
|
26
|
+
costCents: number;
|
|
27
|
+
}
|
|
28
|
+
type ErrorCode = 'not_found' | 'invalid_key' | 'requires_secret_key' | 'limit_reached' | 'workspace_limit_reached' | 'invalid_request' | 'reservation_expired' | 'reservation_not_pending' | 'internal_error' | 'not_implemented';
|
|
29
|
+
interface ApiError {
|
|
30
|
+
code: ErrorCode;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
interface ApiOk<T> {
|
|
34
|
+
ok: true;
|
|
35
|
+
data: T;
|
|
36
|
+
}
|
|
37
|
+
interface ApiFail {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: ApiError;
|
|
40
|
+
}
|
|
41
|
+
type ApiResponse<T> = ApiOk<T> | ApiFail;
|
|
42
|
+
interface CanUseResponseData {
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
reasons: string[];
|
|
45
|
+
details: {
|
|
46
|
+
groupId: string;
|
|
47
|
+
current: number;
|
|
48
|
+
quota: number;
|
|
49
|
+
resetsAt: string | null;
|
|
50
|
+
}[];
|
|
51
|
+
}
|
|
52
|
+
interface ReserveResponseData {
|
|
53
|
+
allowed: boolean;
|
|
54
|
+
reservationId?: string;
|
|
55
|
+
expiresAt?: string;
|
|
56
|
+
reasons?: string[];
|
|
57
|
+
}
|
|
58
|
+
interface UsageResponseData {
|
|
59
|
+
userId: string;
|
|
60
|
+
period: {
|
|
61
|
+
start: string;
|
|
62
|
+
end: string | null;
|
|
63
|
+
} | null;
|
|
64
|
+
counters: CounterSummary[];
|
|
65
|
+
}
|
|
66
|
+
interface UpsertSubscriptionRequest {
|
|
67
|
+
userId: string;
|
|
68
|
+
planId: string;
|
|
69
|
+
customLimits?: PlanLimits;
|
|
70
|
+
endsAt?: string;
|
|
71
|
+
}
|
|
72
|
+
interface UpsertSubscriptionResponseData {
|
|
73
|
+
subscriptionId: string;
|
|
74
|
+
userId: string;
|
|
75
|
+
planId: string;
|
|
76
|
+
startedAt: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ClientOptions {
|
|
80
|
+
apiKey: string;
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
declare class AiPricingLabClient {
|
|
84
|
+
private readonly apiKey;
|
|
85
|
+
private readonly baseUrl;
|
|
86
|
+
constructor(opts: ClientOptions);
|
|
87
|
+
/**
|
|
88
|
+
* Record that a user consumed something.
|
|
89
|
+
*
|
|
90
|
+
* `metadata` is matched against the `metadata` filters on plan match rules.
|
|
91
|
+
* The dashboard uses one reserved key — **`variant`** — when a plan row gates
|
|
92
|
+
* a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,
|
|
93
|
+
* video tier `standard`/`fast`/`lite`). Pass it through so variant-gated
|
|
94
|
+
* buckets count the event:
|
|
95
|
+
*
|
|
96
|
+
* await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });
|
|
97
|
+
*/
|
|
98
|
+
track(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<TrackResponseData>;
|
|
99
|
+
/**
|
|
100
|
+
* Check if a user is allowed to perform an action.
|
|
101
|
+
* Returns `{ allowed, reasons, details }`.
|
|
102
|
+
* Use `.can()` if you only need the boolean.
|
|
103
|
+
*
|
|
104
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
105
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
106
|
+
*/
|
|
107
|
+
canUse(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<CanUseResponseData>;
|
|
108
|
+
/** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */
|
|
109
|
+
can(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* Atomically reserve quota before doing the AI call.
|
|
112
|
+
* On success call `commit(reservationId)`, on failure call `release(reservationId)`.
|
|
113
|
+
*
|
|
114
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
115
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
116
|
+
*/
|
|
117
|
+
reserve(userId: string, event: string, quantity?: number, metadata?: EventMetadata): Promise<ReserveResponseData>;
|
|
118
|
+
/** Confirm a reservation after a successful AI call. */
|
|
119
|
+
commit(reservationId: string): Promise<void>;
|
|
120
|
+
/** Cancel a reservation after a failed AI call so quota is returned. */
|
|
121
|
+
release(reservationId: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Get current usage counters for a user.
|
|
124
|
+
*
|
|
125
|
+
* Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint
|
|
126
|
+
* is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.
|
|
127
|
+
*/
|
|
128
|
+
usage(userId: string, event?: string): Promise<UsageResponseData>;
|
|
129
|
+
/** Assign or update a user's plan subscription. */
|
|
130
|
+
upsertSubscription(params: UpsertSubscriptionRequest): Promise<UpsertSubscriptionResponseData>;
|
|
131
|
+
}
|
|
132
|
+
declare function createClient(opts: ClientOptions): AiPricingLabClient;
|
|
133
|
+
|
|
134
|
+
declare class AiPricingLabError extends Error {
|
|
135
|
+
readonly code: ErrorCode;
|
|
136
|
+
readonly status: number;
|
|
137
|
+
constructor(opts: {
|
|
138
|
+
code: ErrorCode;
|
|
139
|
+
message: string;
|
|
140
|
+
status: number;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { AiPricingLabClient, AiPricingLabError, type ApiError, type ApiResponse, type CanUseResponseData, type ClientOptions, type CounterSummary, type ErrorCode, type EventMetadata, type LimitGroup, type LimitUnit, type MatchRule, type PeriodAnchor, type PeriodType, type PlanLimits, type ReserveResponseData, type TrackResponseData, type UpsertSubscriptionRequest, type UpsertSubscriptionResponseData, type UsageResponseData, createClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
AiPricingLabClient: () => AiPricingLabClient,
|
|
24
|
+
AiPricingLabError: () => AiPricingLabError,
|
|
25
|
+
createClient: () => createClient
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var AiPricingLabError = class extends Error {
|
|
31
|
+
code;
|
|
32
|
+
status;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
super(opts.message);
|
|
35
|
+
this.name = "AiPricingLabError";
|
|
36
|
+
this.code = opts.code;
|
|
37
|
+
this.status = opts.status;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/http.ts
|
|
42
|
+
async function request(baseUrl, apiKey, method, path, body) {
|
|
43
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
method,
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Authorization: `Bearer ${apiKey}`
|
|
49
|
+
},
|
|
50
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
51
|
+
});
|
|
52
|
+
const json = await res.json();
|
|
53
|
+
if (!json.ok) {
|
|
54
|
+
throw new AiPricingLabError({
|
|
55
|
+
code: json.error.code,
|
|
56
|
+
message: json.error.message,
|
|
57
|
+
status: res.status
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return json.data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/client.ts
|
|
64
|
+
var AiPricingLabClient = class {
|
|
65
|
+
apiKey;
|
|
66
|
+
baseUrl;
|
|
67
|
+
constructor(opts) {
|
|
68
|
+
this.apiKey = opts.apiKey;
|
|
69
|
+
this.baseUrl = opts.baseUrl ?? "https://www.aipricinglab.space";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Record that a user consumed something.
|
|
73
|
+
*
|
|
74
|
+
* `metadata` is matched against the `metadata` filters on plan match rules.
|
|
75
|
+
* The dashboard uses one reserved key — **`variant`** — when a plan row gates
|
|
76
|
+
* a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,
|
|
77
|
+
* video tier `standard`/`fast`/`lite`). Pass it through so variant-gated
|
|
78
|
+
* buckets count the event:
|
|
79
|
+
*
|
|
80
|
+
* await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });
|
|
81
|
+
*/
|
|
82
|
+
async track(userId, event, quantity = 1, metadata) {
|
|
83
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/track", {
|
|
84
|
+
userId,
|
|
85
|
+
event,
|
|
86
|
+
quantity,
|
|
87
|
+
metadata
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a user is allowed to perform an action.
|
|
92
|
+
* Returns `{ allowed, reasons, details }`.
|
|
93
|
+
* Use `.can()` if you only need the boolean.
|
|
94
|
+
*
|
|
95
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
96
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
97
|
+
*/
|
|
98
|
+
async canUse(userId, event, quantity = 1, metadata) {
|
|
99
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/can-use", {
|
|
100
|
+
userId,
|
|
101
|
+
event,
|
|
102
|
+
quantity,
|
|
103
|
+
metadata
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */
|
|
107
|
+
async can(userId, event, quantity = 1, metadata) {
|
|
108
|
+
const { allowed } = await this.canUse(userId, event, quantity, metadata);
|
|
109
|
+
return allowed;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Atomically reserve quota before doing the AI call.
|
|
113
|
+
* On success call `commit(reservationId)`, on failure call `release(reservationId)`.
|
|
114
|
+
*
|
|
115
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
116
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
117
|
+
*/
|
|
118
|
+
async reserve(userId, event, quantity = 1, metadata) {
|
|
119
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/reserve", {
|
|
120
|
+
userId,
|
|
121
|
+
event,
|
|
122
|
+
quantity,
|
|
123
|
+
metadata
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Confirm a reservation after a successful AI call. */
|
|
127
|
+
async commit(reservationId) {
|
|
128
|
+
await request(this.baseUrl, this.apiKey, "POST", "/api/v1/commit", {
|
|
129
|
+
reservationId
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Cancel a reservation after a failed AI call so quota is returned. */
|
|
133
|
+
async release(reservationId) {
|
|
134
|
+
await request(this.baseUrl, this.apiKey, "POST", "/api/v1/release", {
|
|
135
|
+
reservationId
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get current usage counters for a user.
|
|
140
|
+
*
|
|
141
|
+
* Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint
|
|
142
|
+
* is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.
|
|
143
|
+
*/
|
|
144
|
+
async usage(userId, event) {
|
|
145
|
+
const qs = new URLSearchParams({ userId });
|
|
146
|
+
if (event) qs.set("event", event);
|
|
147
|
+
const path = this.apiKey.startsWith("pk_live_") ? `/api/v1/usage/me?${qs}` : `/api/v1/usage?${qs}`;
|
|
148
|
+
return request(this.baseUrl, this.apiKey, "GET", path);
|
|
149
|
+
}
|
|
150
|
+
/** Assign or update a user's plan subscription. */
|
|
151
|
+
async upsertSubscription(params) {
|
|
152
|
+
return request(
|
|
153
|
+
this.baseUrl,
|
|
154
|
+
this.apiKey,
|
|
155
|
+
"POST",
|
|
156
|
+
"/api/v1/subscriptions",
|
|
157
|
+
params
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function createClient(opts) {
|
|
162
|
+
return new AiPricingLabClient(opts);
|
|
163
|
+
}
|
|
164
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
165
|
+
0 && (module.exports = {
|
|
166
|
+
AiPricingLabClient,
|
|
167
|
+
AiPricingLabError,
|
|
168
|
+
createClient
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/http.ts","../src/client.ts"],"sourcesContent":["export { AiPricingLabClient, createClient } from './client';\nexport type { ClientOptions } from './client';\nexport { AiPricingLabError } from './errors';\nexport type {\n EventMetadata,\n TrackResponseData,\n CounterSummary,\n CanUseResponseData,\n ReserveResponseData,\n UsageResponseData,\n UpsertSubscriptionRequest,\n UpsertSubscriptionResponseData,\n LimitGroup,\n LimitUnit,\n MatchRule,\n PlanLimits,\n PeriodType,\n PeriodAnchor,\n ErrorCode,\n ApiError,\n ApiResponse,\n} from './types';\n","import type { ErrorCode } from './types';\n\nexport class AiPricingLabError extends Error {\n readonly code: ErrorCode;\n readonly status: number;\n\n constructor(opts: { code: ErrorCode; message: string; status: number }) {\n super(opts.message);\n this.name = 'AiPricingLabError';\n this.code = opts.code;\n this.status = opts.status;\n }\n}\n","// Phase 9: internal fetch wrapper\nimport type { ApiResponse, ErrorCode } from './types';\nimport { AiPricingLabError } from './errors';\n\nexport async function request<T>(\n baseUrl: string,\n apiKey: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n const url = `${baseUrl.replace(/\\/$/, '')}${path}`;\n const res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n\n const json = (await res.json()) as ApiResponse<T>;\n\n if (!json.ok) {\n throw new AiPricingLabError({\n code: json.error.code as ErrorCode,\n message: json.error.message,\n status: res.status,\n });\n }\n\n return json.data;\n}\n","import type {\n TrackResponseData,\n CanUseResponseData,\n ReserveResponseData,\n UsageResponseData,\n UpsertSubscriptionRequest,\n UpsertSubscriptionResponseData,\n EventMetadata,\n} from './types';\nimport { request } from './http';\n\nexport interface ClientOptions {\n apiKey: string;\n baseUrl?: string;\n}\n\nexport class AiPricingLabClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(opts: ClientOptions) {\n this.apiKey = opts.apiKey;\n this.baseUrl = opts.baseUrl ?? 'https://www.aipricinglab.space';\n }\n\n /**\n * Record that a user consumed something.\n *\n * `metadata` is matched against the `metadata` filters on plan match rules.\n * The dashboard uses one reserved key — **`variant`** — when a plan row gates\n * a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,\n * video tier `standard`/`fast`/`lite`). Pass it through so variant-gated\n * buckets count the event:\n *\n * await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });\n */\n async track(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<TrackResponseData> {\n return request<TrackResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/track', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /**\n * Check if a user is allowed to perform an action.\n * Returns `{ allowed, reasons, details }`.\n * Use `.can()` if you only need the boolean.\n *\n * Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates\n * the model to a specific output tier — see {@link AiPricingLabClient.track}.\n */\n async canUse(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<CanUseResponseData> {\n return request<CanUseResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/can-use', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */\n async can(userId: string, event: string, quantity = 1, metadata?: EventMetadata): Promise<boolean> {\n const { allowed } = await this.canUse(userId, event, quantity, metadata);\n return allowed;\n }\n\n /**\n * Atomically reserve quota before doing the AI call.\n * On success call `commit(reservationId)`, on failure call `release(reservationId)`.\n *\n * Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates\n * the model to a specific output tier — see {@link AiPricingLabClient.track}.\n */\n async reserve(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<ReserveResponseData> {\n return request<ReserveResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/reserve', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /** Confirm a reservation after a successful AI call. */\n async commit(reservationId: string): Promise<void> {\n await request<Record<string, never>>(this.baseUrl, this.apiKey, 'POST', '/api/v1/commit', {\n reservationId,\n });\n }\n\n /** Cancel a reservation after a failed AI call so quota is returned. */\n async release(reservationId: string): Promise<void> {\n await request<Record<string, never>>(this.baseUrl, this.apiKey, 'POST', '/api/v1/release', {\n reservationId,\n });\n }\n\n /**\n * Get current usage counters for a user.\n *\n * Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint\n * is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.\n */\n async usage(userId: string, event?: string): Promise<UsageResponseData> {\n const qs = new URLSearchParams({ userId });\n if (event) qs.set('event', event);\n const path = this.apiKey.startsWith('pk_live_')\n ? `/api/v1/usage/me?${qs}`\n : `/api/v1/usage?${qs}`;\n return request<UsageResponseData>(this.baseUrl, this.apiKey, 'GET', path);\n }\n\n /** Assign or update a user's plan subscription. */\n async upsertSubscription(\n params: UpsertSubscriptionRequest,\n ): Promise<UpsertSubscriptionResponseData> {\n return request<UpsertSubscriptionResponseData>(\n this.baseUrl,\n this.apiKey,\n 'POST',\n '/api/v1/subscriptions',\n params,\n );\n }\n}\n\nexport function createClient(opts: ClientOptions): AiPricingLabClient {\n return new AiPricingLabClient(opts);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var AiPricingLabError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
super(opts.message);
|
|
7
|
+
this.name = "AiPricingLabError";
|
|
8
|
+
this.code = opts.code;
|
|
9
|
+
this.status = opts.status;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/http.ts
|
|
14
|
+
async function request(baseUrl, apiKey, method, path, body) {
|
|
15
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
method,
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Authorization: `Bearer ${apiKey}`
|
|
21
|
+
},
|
|
22
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
23
|
+
});
|
|
24
|
+
const json = await res.json();
|
|
25
|
+
if (!json.ok) {
|
|
26
|
+
throw new AiPricingLabError({
|
|
27
|
+
code: json.error.code,
|
|
28
|
+
message: json.error.message,
|
|
29
|
+
status: res.status
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return json.data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/client.ts
|
|
36
|
+
var AiPricingLabClient = class {
|
|
37
|
+
apiKey;
|
|
38
|
+
baseUrl;
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this.apiKey = opts.apiKey;
|
|
41
|
+
this.baseUrl = opts.baseUrl ?? "https://www.aipricinglab.space";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Record that a user consumed something.
|
|
45
|
+
*
|
|
46
|
+
* `metadata` is matched against the `metadata` filters on plan match rules.
|
|
47
|
+
* The dashboard uses one reserved key — **`variant`** — when a plan row gates
|
|
48
|
+
* a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,
|
|
49
|
+
* video tier `standard`/`fast`/`lite`). Pass it through so variant-gated
|
|
50
|
+
* buckets count the event:
|
|
51
|
+
*
|
|
52
|
+
* await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });
|
|
53
|
+
*/
|
|
54
|
+
async track(userId, event, quantity = 1, metadata) {
|
|
55
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/track", {
|
|
56
|
+
userId,
|
|
57
|
+
event,
|
|
58
|
+
quantity,
|
|
59
|
+
metadata
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if a user is allowed to perform an action.
|
|
64
|
+
* Returns `{ allowed, reasons, details }`.
|
|
65
|
+
* Use `.can()` if you only need the boolean.
|
|
66
|
+
*
|
|
67
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
68
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
69
|
+
*/
|
|
70
|
+
async canUse(userId, event, quantity = 1, metadata) {
|
|
71
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/can-use", {
|
|
72
|
+
userId,
|
|
73
|
+
event,
|
|
74
|
+
quantity,
|
|
75
|
+
metadata
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */
|
|
79
|
+
async can(userId, event, quantity = 1, metadata) {
|
|
80
|
+
const { allowed } = await this.canUse(userId, event, quantity, metadata);
|
|
81
|
+
return allowed;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Atomically reserve quota before doing the AI call.
|
|
85
|
+
* On success call `commit(reservationId)`, on failure call `release(reservationId)`.
|
|
86
|
+
*
|
|
87
|
+
* Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates
|
|
88
|
+
* the model to a specific output tier — see {@link AiPricingLabClient.track}.
|
|
89
|
+
*/
|
|
90
|
+
async reserve(userId, event, quantity = 1, metadata) {
|
|
91
|
+
return request(this.baseUrl, this.apiKey, "POST", "/api/v1/reserve", {
|
|
92
|
+
userId,
|
|
93
|
+
event,
|
|
94
|
+
quantity,
|
|
95
|
+
metadata
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Confirm a reservation after a successful AI call. */
|
|
99
|
+
async commit(reservationId) {
|
|
100
|
+
await request(this.baseUrl, this.apiKey, "POST", "/api/v1/commit", {
|
|
101
|
+
reservationId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Cancel a reservation after a failed AI call so quota is returned. */
|
|
105
|
+
async release(reservationId) {
|
|
106
|
+
await request(this.baseUrl, this.apiKey, "POST", "/api/v1/release", {
|
|
107
|
+
reservationId
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get current usage counters for a user.
|
|
112
|
+
*
|
|
113
|
+
* Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint
|
|
114
|
+
* is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.
|
|
115
|
+
*/
|
|
116
|
+
async usage(userId, event) {
|
|
117
|
+
const qs = new URLSearchParams({ userId });
|
|
118
|
+
if (event) qs.set("event", event);
|
|
119
|
+
const path = this.apiKey.startsWith("pk_live_") ? `/api/v1/usage/me?${qs}` : `/api/v1/usage?${qs}`;
|
|
120
|
+
return request(this.baseUrl, this.apiKey, "GET", path);
|
|
121
|
+
}
|
|
122
|
+
/** Assign or update a user's plan subscription. */
|
|
123
|
+
async upsertSubscription(params) {
|
|
124
|
+
return request(
|
|
125
|
+
this.baseUrl,
|
|
126
|
+
this.apiKey,
|
|
127
|
+
"POST",
|
|
128
|
+
"/api/v1/subscriptions",
|
|
129
|
+
params
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
function createClient(opts) {
|
|
134
|
+
return new AiPricingLabClient(opts);
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
AiPricingLabClient,
|
|
138
|
+
AiPricingLabError,
|
|
139
|
+
createClient
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/client.ts"],"sourcesContent":["import type { ErrorCode } from './types';\n\nexport class AiPricingLabError extends Error {\n readonly code: ErrorCode;\n readonly status: number;\n\n constructor(opts: { code: ErrorCode; message: string; status: number }) {\n super(opts.message);\n this.name = 'AiPricingLabError';\n this.code = opts.code;\n this.status = opts.status;\n }\n}\n","// Phase 9: internal fetch wrapper\nimport type { ApiResponse, ErrorCode } from './types';\nimport { AiPricingLabError } from './errors';\n\nexport async function request<T>(\n baseUrl: string,\n apiKey: string,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n const url = `${baseUrl.replace(/\\/$/, '')}${path}`;\n const res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n\n const json = (await res.json()) as ApiResponse<T>;\n\n if (!json.ok) {\n throw new AiPricingLabError({\n code: json.error.code as ErrorCode,\n message: json.error.message,\n status: res.status,\n });\n }\n\n return json.data;\n}\n","import type {\n TrackResponseData,\n CanUseResponseData,\n ReserveResponseData,\n UsageResponseData,\n UpsertSubscriptionRequest,\n UpsertSubscriptionResponseData,\n EventMetadata,\n} from './types';\nimport { request } from './http';\n\nexport interface ClientOptions {\n apiKey: string;\n baseUrl?: string;\n}\n\nexport class AiPricingLabClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(opts: ClientOptions) {\n this.apiKey = opts.apiKey;\n this.baseUrl = opts.baseUrl ?? 'https://www.aipricinglab.space';\n }\n\n /**\n * Record that a user consumed something.\n *\n * `metadata` is matched against the `metadata` filters on plan match rules.\n * The dashboard uses one reserved key — **`variant`** — when a plan row gates\n * a model to a specific output tier (e.g. image quality `1k`/`2k`/`4k`,\n * video tier `standard`/`fast`/`lite`). Pass it through so variant-gated\n * buckets count the event:\n *\n * await apl.track('user_123', 'image.gemini-3.1-image', 1, { variant: '4k' });\n */\n async track(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<TrackResponseData> {\n return request<TrackResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/track', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /**\n * Check if a user is allowed to perform an action.\n * Returns `{ allowed, reasons, details }`.\n * Use `.can()` if you only need the boolean.\n *\n * Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates\n * the model to a specific output tier — see {@link AiPricingLabClient.track}.\n */\n async canUse(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<CanUseResponseData> {\n return request<CanUseResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/can-use', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /** Shorthand: returns `true` if allowed, `false` if the user hit a limit. */\n async can(userId: string, event: string, quantity = 1, metadata?: EventMetadata): Promise<boolean> {\n const { allowed } = await this.canUse(userId, event, quantity, metadata);\n return allowed;\n }\n\n /**\n * Atomically reserve quota before doing the AI call.\n * On success call `commit(reservationId)`, on failure call `release(reservationId)`.\n *\n * Pass `metadata.variant` (e.g. `'4k'`, `'standard'`) when a plan row gates\n * the model to a specific output tier — see {@link AiPricingLabClient.track}.\n */\n async reserve(\n userId: string,\n event: string,\n quantity = 1,\n metadata?: EventMetadata,\n ): Promise<ReserveResponseData> {\n return request<ReserveResponseData>(this.baseUrl, this.apiKey, 'POST', '/api/v1/reserve', {\n userId,\n event,\n quantity,\n metadata,\n });\n }\n\n /** Confirm a reservation after a successful AI call. */\n async commit(reservationId: string): Promise<void> {\n await request<Record<string, never>>(this.baseUrl, this.apiKey, 'POST', '/api/v1/commit', {\n reservationId,\n });\n }\n\n /** Cancel a reservation after a failed AI call so quota is returned. */\n async release(reservationId: string): Promise<void> {\n await request<Record<string, never>>(this.baseUrl, this.apiKey, 'POST', '/api/v1/release', {\n reservationId,\n });\n }\n\n /**\n * Get current usage counters for a user.\n *\n * Routes to `/api/v1/usage/me` for public (`pk_live_`) keys — that endpoint\n * is the only one that accepts public keys. Secret keys hit `/api/v1/usage`.\n */\n async usage(userId: string, event?: string): Promise<UsageResponseData> {\n const qs = new URLSearchParams({ userId });\n if (event) qs.set('event', event);\n const path = this.apiKey.startsWith('pk_live_')\n ? `/api/v1/usage/me?${qs}`\n : `/api/v1/usage?${qs}`;\n return request<UsageResponseData>(this.baseUrl, this.apiKey, 'GET', path);\n }\n\n /** Assign or update a user's plan subscription. */\n async upsertSubscription(\n params: UpsertSubscriptionRequest,\n ): Promise<UpsertSubscriptionResponseData> {\n return request<UpsertSubscriptionResponseData>(\n this.baseUrl,\n this.apiKey,\n 'POST',\n '/api/v1/subscriptions',\n params,\n );\n }\n}\n\nexport function createClient(opts: ClientOptions): AiPricingLabClient {\n return new AiPricingLabClient(opts);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aipricinglab/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Usage metering and limits SDK for AI-powered apps. Track LLM tokens, image generations, video seconds, and any other AI consumption — provider-agnostic.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"metering",
|
|
8
|
+
"usage",
|
|
9
|
+
"limits",
|
|
10
|
+
"billing",
|
|
11
|
+
"rate-limiting",
|
|
12
|
+
"llm",
|
|
13
|
+
"tokens",
|
|
14
|
+
"openai",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"stripe",
|
|
17
|
+
"saas"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://aipricinglab.space",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://aipricinglab.space/support"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "AiPricingLab",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": ["dist", "README.md"],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "pnpm build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.5.0",
|
|
47
|
+
"typescript": "^5.8.3"
|
|
48
|
+
}
|
|
49
|
+
}
|