@drawbridge/drawbridge-utils 0.0.29 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.cjs CHANGED
@@ -19,6 +19,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // ai.js
20
20
  var ai_exports = {};
21
21
  __export(ai_exports, {
22
+ MARKUP: () => MARKUP,
23
+ cost: () => cost,
24
+ costForRequest: () => costForRequest,
22
25
  google: () => google,
23
26
  priceForRequest: () => priceForRequest,
24
27
  pricing: () => pricing
@@ -78,105 +81,76 @@ var circuit = ({
78
81
  };
79
82
 
80
83
  // transactions.js
81
- var recordTransaction = async ({
84
+ var insertTransaction = async ({
82
85
  db,
83
- authenticated,
86
+ user,
84
87
  userId,
88
+ type,
85
89
  category,
86
- tags,
90
+ source,
87
91
  amount,
88
- balance,
89
- provider,
92
+ _id,
90
93
  stripeInvoiceId,
91
94
  stripeEventId,
92
- session
95
+ session,
96
+ ...rest
93
97
  }) => {
98
+ var _a;
99
+ const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
100
+ const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
94
101
  await db.create({
95
- authenticated,
102
+ authenticated: user,
96
103
  collection: "transaction",
97
104
  data: {
105
+ ..._id && { _id },
98
106
  userId,
99
- billable: {
100
- item: "ai"
101
- },
107
+ type,
102
108
  category,
103
- tags,
109
+ source,
104
110
  amount,
105
- balance,
106
- ...provider && { provider },
111
+ ...balance && { balance },
112
+ ...rest,
107
113
  ...stripeInvoiceId && { stripeInvoiceId },
108
114
  ...stripeEventId && { stripeEventId }
109
115
  },
110
116
  ...session && { options: { session } }
111
117
  });
112
118
  };
113
- var InsufficientCreditsError = class extends Error {
114
- constructor(message = "Insufficient AI credits") {
115
- super(message);
116
- this.name = "InsufficientCreditsError";
117
- this.status = 402;
118
- }
119
- };
120
119
  var debit = async ({
121
120
  db,
122
- authenticated,
121
+ user,
123
122
  userId,
124
123
  amount,
124
+ type,
125
125
  category,
126
- tags = [],
127
- provider,
126
+ source,
128
127
  stripeInvoiceId,
129
128
  stripeEventId,
130
- session
129
+ session,
130
+ ...rest
131
131
  }) => {
132
- var _a, _b, _c;
133
132
  if (!Number.isInteger(amount) || amount <= 0) {
134
133
  throw new Error(`debit() requires a positive integer amount, got ${amount}`);
135
134
  }
136
- const result = await db.update({
137
- authenticated,
138
- collection: "user",
139
- query: {
140
- id: userId,
141
- "balance.ai": {
142
- $gte: amount
143
- }
144
- },
145
- data: {
146
- $inc: {
147
- "balance.ai": -amount
148
- }
149
- },
150
- ...session && { options: { session } }
151
- });
152
- const after = ((_b = (_a = result == null ? void 0 : result.value) == null ? void 0 : _a.balance) == null ? void 0 : _b.ai) ?? ((_c = result == null ? void 0 : result.balance) == null ? void 0 : _c.ai);
153
- if (typeof after !== "number") {
154
- throw new InsufficientCreditsError();
135
+ if (!type) {
136
+ throw new Error('debit() requires a type (e.g., "ai")');
155
137
  }
156
- const before = after + amount;
157
- await recordTransaction({
138
+ if (!source) {
139
+ throw new Error('debit() requires a source ("system" | "admin" | "user")');
140
+ }
141
+ await insertTransaction({
158
142
  db,
159
- authenticated,
143
+ user,
160
144
  userId,
145
+ type,
161
146
  category,
162
- tags,
147
+ source,
163
148
  amount: -amount,
164
- balance: {
165
- before,
166
- after
167
- },
168
- provider,
169
149
  stripeInvoiceId,
170
150
  stripeEventId,
171
- session
151
+ session,
152
+ ...rest
172
153
  });
173
- return {
174
- amount: -amount,
175
- balance: {
176
- before,
177
- after
178
- }
179
- };
180
154
  };
181
155
 
182
156
  // ai.js
@@ -192,7 +166,7 @@ var models = {
192
166
  image: "gemini-2.5-flash-image",
193
167
  text: "gemini-2.5-flash"
194
168
  };
195
- var pricing = {
169
+ var cost = {
196
170
  "gemini-2.5-flash": {
197
171
  cached: 3,
198
172
  input: 30,
@@ -204,18 +178,33 @@ var pricing = {
204
178
  output: 250
205
179
  }
206
180
  };
181
+ var MARKUP = 1.3;
182
+ var pricing = Object.fromEntries(
183
+ Object.entries(cost).map(([model, rates]) => [
184
+ model,
185
+ {
186
+ cached: Math.round(rates.cached * MARKUP),
187
+ input: Math.round(rates.input * MARKUP),
188
+ output: Math.round(rates.output * MARKUP)
189
+ }
190
+ ])
191
+ );
192
+ var sumCents = (rates, tokens) => Math.ceil(
193
+ (Number((tokens == null ? void 0 : tokens.cached) || 0) * rates.cached + Number((tokens == null ? void 0 : tokens.input) || 0) * rates.input + Number((tokens == null ? void 0 : tokens.output) || 0) * rates.output) / 1e6
194
+ );
207
195
  var priceForRequest = (model, usage) => {
208
196
  const rates = pricing[model];
209
197
  if (!rates) {
210
198
  throw new Error(`Unknown model for pricing: ${model}`);
211
199
  }
212
- const tokens = {
213
- cached: Number(usage == null ? void 0 : usage.cached) || 0,
214
- input: Number(usage == null ? void 0 : usage.input) || 0,
215
- output: Number(usage == null ? void 0 : usage.output) || 0
216
- };
217
- const sum = tokens.cached * rates.cached + tokens.input * rates.input + tokens.output * rates.output;
218
- return Math.ceil(sum / 1e6);
200
+ return sumCents(rates, usage);
201
+ };
202
+ var costForRequest = (model, usage) => {
203
+ const rates = cost[model];
204
+ if (!rates) {
205
+ throw new Error(`Unknown model for cost: ${model}`);
206
+ }
207
+ return sumCents(rates, usage);
219
208
  };
220
209
  var tokensFromMetadata = (metadata) => ({
221
210
  cached: Number(metadata == null ? void 0 : metadata.cachedContentTokenCount) || 0,
@@ -229,19 +218,29 @@ var billRequest = async ({
229
218
  model,
230
219
  usage
231
220
  }) => {
232
- const amount = priceForRequest(model, usage);
221
+ const gross = priceForRequest(model, usage);
222
+ const cogs = costForRequest(model, usage);
233
223
  await debit({
234
224
  db,
235
225
  authenticated,
236
226
  userId,
237
- amount,
227
+ amount: gross,
228
+ type: "ai",
238
229
  category: "usage",
239
- tags: ["google", model],
240
- provider: {
230
+ source: "user",
231
+ ai: {
241
232
  name: "google",
242
233
  model,
243
234
  tokens: usage,
244
- snapshot: pricing[model]
235
+ rates: {
236
+ wholesale: cost[model],
237
+ retail: pricing[model]
238
+ },
239
+ totals: {
240
+ cost: cogs,
241
+ net: gross - cogs,
242
+ gross
243
+ }
245
244
  }
246
245
  });
247
246
  };
@@ -327,6 +326,9 @@ var google = {
327
326
  };
328
327
  // Annotate the CommonJS export names for ESM import in node:
329
328
  0 && (module.exports = {
329
+ MARKUP,
330
+ cost,
331
+ costForRequest,
330
332
  google,
331
333
  priceForRequest,
332
334
  pricing
package/dist/ai.d.cts CHANGED
@@ -36,14 +36,12 @@ const models = {
36
36
  text : 'gemini-2.5-flash'
37
37
  };
38
38
 
39
- // Per-model pricing in cents per 1,000,000 tokens. Source of truth for what
40
- // users get charged; reviewed in PRs.
41
- //
42
- // Values mirror LiteLLM's published pricing at the time of writing. Model
43
- // providers update pricing periodically — verify against
39
+ // Raw per-model provider rates in cents per 1,000,000 tokens. This is what
40
+ // Drawbridge pays the provider the wholesale cost. Sync from LiteLLM:
44
41
  // https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json
45
- // before any change.
46
- const pricing = {
42
+ // Never apply markup here; the MARKUP constant below derives the retail
43
+ // `pricing` table.
44
+ const cost = {
47
45
  'gemini-2.5-flash' : {
48
46
  cached : 3,
49
47
  input : 30,
@@ -56,9 +54,37 @@ const pricing = {
56
54
  }
57
55
  };
58
56
 
59
- // Cost in cents for a single request, rounded up so we never charge fractional
60
- // cents and never under-bill. Throws on an unknown model so a typo doesn't
61
- // silently fall back to a wrong price.
57
+ // Markup multiplier applied to every model's cost to produce the retail
58
+ // price. 1.3 = 30% margin. Change one number, all pricing updates. Mirrors
59
+ // the per-customer markup Stripe rate cards used to provide; keeping it
60
+ // global (rather than per-model or per-plan) for simplicity — easy to
61
+ // refactor later if tier-based pricing becomes a real need.
62
+ const MARKUP = 1.3;
63
+
64
+ // Derived retail pricing in cents per 1,000,000 tokens — what users pay.
65
+ // Rounded so the per-1M rate is a whole-cent value; per-request rounding
66
+ // happens in priceForRequest via Math.ceil so we never under-bill.
67
+ const pricing = Object.fromEntries(
68
+ Object.entries( cost ).map( ( [ model, rates ] ) => [
69
+ model,
70
+ {
71
+ cached : Math.round( rates.cached * MARKUP ),
72
+ input : Math.round( rates.input * MARKUP ),
73
+ output : Math.round( rates.output * MARKUP )
74
+ }
75
+ ] )
76
+ );
77
+
78
+ const sumCents = ( rates, tokens ) => Math.ceil(
79
+ (
80
+ Number( tokens?.cached || 0 ) * rates.cached +
81
+ Number( tokens?.input || 0 ) * rates.input +
82
+ Number( tokens?.output || 0 ) * rates.output
83
+ ) / 1_000_000
84
+ );
85
+
86
+ // What we charge the user, rounded up so we never under-bill. Throws on an
87
+ // unknown model so a typo doesn't silently fall back to a wrong price.
62
88
  const priceForRequest = ( model, usage ) => {
63
89
 
64
90
  const rates = pricing[ model ];
@@ -69,19 +95,23 @@ const priceForRequest = ( model, usage ) => {
69
95
 
70
96
  }
71
97
 
72
- const tokens = {
73
- cached : Number( usage?.cached ) || 0,
74
- input : Number( usage?.input ) || 0,
75
- output : Number( usage?.output ) || 0
76
- };
98
+ return sumCents( rates, usage );
99
+
100
+ };
101
+
102
+ // What we pay the provider. Recorded on each transaction row so margin is
103
+ // computable per request without backfilling from git history.
104
+ const costForRequest = ( model, usage ) => {
77
105
 
78
- const sum = (
79
- tokens.cached * rates.cached +
80
- tokens.input * rates.input +
81
- tokens.output * rates.output
82
- );
106
+ const rates = cost[ model ];
83
107
 
84
- return Math.ceil( sum / 1_000_000 );
108
+ if( !rates ){
109
+
110
+ throw new Error( `Unknown model for cost: ${ model }` );
111
+
112
+ }
113
+
114
+ return sumCents( rates, usage );
85
115
 
86
116
  };
87
117
 
@@ -92,8 +122,11 @@ const tokensFromMetadata = ( metadata ) => ({
92
122
  output : Number( metadata?.candidatesTokenCount ) || 0
93
123
  });
94
124
 
95
- // Compute cost from response usage and atomically debit the user. Shared by
96
- // both image/text handlers so the accounting path is identical for both.
125
+ // Compute gross + cost from response usage and atomically debit the user.
126
+ // Records the full income-statement line on the transaction row's `ai`
127
+ // sub-doc: wholesale + retail per-1M rate snapshots, plus rolled-up cost,
128
+ // net, and gross totals. Margin per request is `ai.net`; margin per query
129
+ // is a single $sum: '$ai.net'.
97
130
  const billRequest = async ({
98
131
  db,
99
132
  authenticated,
@@ -102,20 +135,30 @@ const billRequest = async ({
102
135
  usage
103
136
  }) => {
104
137
 
105
- const amount = priceForRequest( model, usage );
138
+ const gross = priceForRequest( model, usage );
139
+ const cogs = costForRequest( model, usage );
106
140
 
107
141
  await debit({
108
142
  db,
109
143
  authenticated,
110
144
  userId,
111
- amount,
145
+ amount : gross,
146
+ type : 'ai',
112
147
  category : 'usage',
113
- tags : [ 'google', model ],
114
- provider : {
148
+ source : 'user',
149
+ ai : {
115
150
  name : 'google',
116
151
  model,
117
152
  tokens : usage,
118
- snapshot : pricing[ model ]
153
+ rates : {
154
+ wholesale : cost[ model ],
155
+ retail : pricing[ model ]
156
+ },
157
+ totals : {
158
+ cost : cogs,
159
+ net : gross - cogs,
160
+ gross
161
+ }
119
162
  }
120
163
  });
121
164
 
@@ -241,4 +284,4 @@ const google = {
241
284
 
242
285
  };
243
286
 
244
- export { google, priceForRequest, pricing };
287
+ export { MARKUP, cost, costForRequest, google, priceForRequest, pricing };
package/dist/ai.d.ts CHANGED
@@ -36,14 +36,12 @@ const models = {
36
36
  text : 'gemini-2.5-flash'
37
37
  };
38
38
 
39
- // Per-model pricing in cents per 1,000,000 tokens. Source of truth for what
40
- // users get charged; reviewed in PRs.
41
- //
42
- // Values mirror LiteLLM's published pricing at the time of writing. Model
43
- // providers update pricing periodically — verify against
39
+ // Raw per-model provider rates in cents per 1,000,000 tokens. This is what
40
+ // Drawbridge pays the provider the wholesale cost. Sync from LiteLLM:
44
41
  // https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json
45
- // before any change.
46
- const pricing = {
42
+ // Never apply markup here; the MARKUP constant below derives the retail
43
+ // `pricing` table.
44
+ const cost = {
47
45
  'gemini-2.5-flash' : {
48
46
  cached : 3,
49
47
  input : 30,
@@ -56,9 +54,37 @@ const pricing = {
56
54
  }
57
55
  };
58
56
 
59
- // Cost in cents for a single request, rounded up so we never charge fractional
60
- // cents and never under-bill. Throws on an unknown model so a typo doesn't
61
- // silently fall back to a wrong price.
57
+ // Markup multiplier applied to every model's cost to produce the retail
58
+ // price. 1.3 = 30% margin. Change one number, all pricing updates. Mirrors
59
+ // the per-customer markup Stripe rate cards used to provide; keeping it
60
+ // global (rather than per-model or per-plan) for simplicity — easy to
61
+ // refactor later if tier-based pricing becomes a real need.
62
+ const MARKUP = 1.3;
63
+
64
+ // Derived retail pricing in cents per 1,000,000 tokens — what users pay.
65
+ // Rounded so the per-1M rate is a whole-cent value; per-request rounding
66
+ // happens in priceForRequest via Math.ceil so we never under-bill.
67
+ const pricing = Object.fromEntries(
68
+ Object.entries( cost ).map( ( [ model, rates ] ) => [
69
+ model,
70
+ {
71
+ cached : Math.round( rates.cached * MARKUP ),
72
+ input : Math.round( rates.input * MARKUP ),
73
+ output : Math.round( rates.output * MARKUP )
74
+ }
75
+ ] )
76
+ );
77
+
78
+ const sumCents = ( rates, tokens ) => Math.ceil(
79
+ (
80
+ Number( tokens?.cached || 0 ) * rates.cached +
81
+ Number( tokens?.input || 0 ) * rates.input +
82
+ Number( tokens?.output || 0 ) * rates.output
83
+ ) / 1_000_000
84
+ );
85
+
86
+ // What we charge the user, rounded up so we never under-bill. Throws on an
87
+ // unknown model so a typo doesn't silently fall back to a wrong price.
62
88
  const priceForRequest = ( model, usage ) => {
63
89
 
64
90
  const rates = pricing[ model ];
@@ -69,19 +95,23 @@ const priceForRequest = ( model, usage ) => {
69
95
 
70
96
  }
71
97
 
72
- const tokens = {
73
- cached : Number( usage?.cached ) || 0,
74
- input : Number( usage?.input ) || 0,
75
- output : Number( usage?.output ) || 0
76
- };
98
+ return sumCents( rates, usage );
99
+
100
+ };
101
+
102
+ // What we pay the provider. Recorded on each transaction row so margin is
103
+ // computable per request without backfilling from git history.
104
+ const costForRequest = ( model, usage ) => {
77
105
 
78
- const sum = (
79
- tokens.cached * rates.cached +
80
- tokens.input * rates.input +
81
- tokens.output * rates.output
82
- );
106
+ const rates = cost[ model ];
83
107
 
84
- return Math.ceil( sum / 1_000_000 );
108
+ if( !rates ){
109
+
110
+ throw new Error( `Unknown model for cost: ${ model }` );
111
+
112
+ }
113
+
114
+ return sumCents( rates, usage );
85
115
 
86
116
  };
87
117
 
@@ -92,8 +122,11 @@ const tokensFromMetadata = ( metadata ) => ({
92
122
  output : Number( metadata?.candidatesTokenCount ) || 0
93
123
  });
94
124
 
95
- // Compute cost from response usage and atomically debit the user. Shared by
96
- // both image/text handlers so the accounting path is identical for both.
125
+ // Compute gross + cost from response usage and atomically debit the user.
126
+ // Records the full income-statement line on the transaction row's `ai`
127
+ // sub-doc: wholesale + retail per-1M rate snapshots, plus rolled-up cost,
128
+ // net, and gross totals. Margin per request is `ai.net`; margin per query
129
+ // is a single $sum: '$ai.net'.
97
130
  const billRequest = async ({
98
131
  db,
99
132
  authenticated,
@@ -102,20 +135,30 @@ const billRequest = async ({
102
135
  usage
103
136
  }) => {
104
137
 
105
- const amount = priceForRequest( model, usage );
138
+ const gross = priceForRequest( model, usage );
139
+ const cogs = costForRequest( model, usage );
106
140
 
107
141
  await debit({
108
142
  db,
109
143
  authenticated,
110
144
  userId,
111
- amount,
145
+ amount : gross,
146
+ type : 'ai',
112
147
  category : 'usage',
113
- tags : [ 'google', model ],
114
- provider : {
148
+ source : 'user',
149
+ ai : {
115
150
  name : 'google',
116
151
  model,
117
152
  tokens : usage,
118
- snapshot : pricing[ model ]
153
+ rates : {
154
+ wholesale : cost[ model ],
155
+ retail : pricing[ model ]
156
+ },
157
+ totals : {
158
+ cost : cogs,
159
+ net : gross - cogs,
160
+ gross
161
+ }
119
162
  }
120
163
  });
121
164
 
@@ -241,4 +284,4 @@ const google = {
241
284
 
242
285
  };
243
286
 
244
- export { google, priceForRequest, pricing };
287
+ export { MARKUP, cost, costForRequest, google, priceForRequest, pricing };
package/dist/ai.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debit
3
- } from "./chunk-H735KDYS.js";
3
+ } from "./chunk-6S43OFA7.js";
4
4
  import {
5
5
  circuit
6
6
  } from "./chunk-6HH36OK6.js";
@@ -19,7 +19,7 @@ var models = {
19
19
  image: "gemini-2.5-flash-image",
20
20
  text: "gemini-2.5-flash"
21
21
  };
22
- var pricing = {
22
+ var cost = {
23
23
  "gemini-2.5-flash": {
24
24
  cached: 3,
25
25
  input: 30,
@@ -31,18 +31,33 @@ var pricing = {
31
31
  output: 250
32
32
  }
33
33
  };
34
+ var MARKUP = 1.3;
35
+ var pricing = Object.fromEntries(
36
+ Object.entries(cost).map(([model, rates]) => [
37
+ model,
38
+ {
39
+ cached: Math.round(rates.cached * MARKUP),
40
+ input: Math.round(rates.input * MARKUP),
41
+ output: Math.round(rates.output * MARKUP)
42
+ }
43
+ ])
44
+ );
45
+ var sumCents = (rates, tokens) => Math.ceil(
46
+ (Number((tokens == null ? void 0 : tokens.cached) || 0) * rates.cached + Number((tokens == null ? void 0 : tokens.input) || 0) * rates.input + Number((tokens == null ? void 0 : tokens.output) || 0) * rates.output) / 1e6
47
+ );
34
48
  var priceForRequest = (model, usage) => {
35
49
  const rates = pricing[model];
36
50
  if (!rates) {
37
51
  throw new Error(`Unknown model for pricing: ${model}`);
38
52
  }
39
- const tokens = {
40
- cached: Number(usage == null ? void 0 : usage.cached) || 0,
41
- input: Number(usage == null ? void 0 : usage.input) || 0,
42
- output: Number(usage == null ? void 0 : usage.output) || 0
43
- };
44
- const sum = tokens.cached * rates.cached + tokens.input * rates.input + tokens.output * rates.output;
45
- return Math.ceil(sum / 1e6);
53
+ return sumCents(rates, usage);
54
+ };
55
+ var costForRequest = (model, usage) => {
56
+ const rates = cost[model];
57
+ if (!rates) {
58
+ throw new Error(`Unknown model for cost: ${model}`);
59
+ }
60
+ return sumCents(rates, usage);
46
61
  };
47
62
  var tokensFromMetadata = (metadata) => ({
48
63
  cached: Number(metadata == null ? void 0 : metadata.cachedContentTokenCount) || 0,
@@ -56,19 +71,29 @@ var billRequest = async ({
56
71
  model,
57
72
  usage
58
73
  }) => {
59
- const amount = priceForRequest(model, usage);
74
+ const gross = priceForRequest(model, usage);
75
+ const cogs = costForRequest(model, usage);
60
76
  await debit({
61
77
  db,
62
78
  authenticated,
63
79
  userId,
64
- amount,
80
+ amount: gross,
81
+ type: "ai",
65
82
  category: "usage",
66
- tags: ["google", model],
67
- provider: {
83
+ source: "user",
84
+ ai: {
68
85
  name: "google",
69
86
  model,
70
87
  tokens: usage,
71
- snapshot: pricing[model]
88
+ rates: {
89
+ wholesale: cost[model],
90
+ retail: pricing[model]
91
+ },
92
+ totals: {
93
+ cost: cogs,
94
+ net: gross - cogs,
95
+ gross
96
+ }
72
97
  }
73
98
  });
74
99
  };
@@ -153,6 +178,9 @@ var google = {
153
178
  }
154
179
  };
155
180
  export {
181
+ MARKUP,
182
+ cost,
183
+ costForRequest,
156
184
  google,
157
185
  priceForRequest,
158
186
  pricing