@drawbridge/drawbridge-utils 0.0.28 → 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,100 +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
- stripeEventId
94
+ stripeEventId,
95
+ session,
96
+ ...rest
92
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;
93
101
  await db.create({
94
- authenticated,
95
- collection: "transactions",
102
+ authenticated: user,
103
+ collection: "transaction",
96
104
  data: {
105
+ ..._id && { _id },
97
106
  userId,
98
- billable: {
99
- item: "ai"
100
- },
107
+ type,
101
108
  category,
102
- tags,
109
+ source,
103
110
  amount,
104
- balance,
105
- ...provider && { provider },
111
+ ...balance && { balance },
112
+ ...rest,
106
113
  ...stripeInvoiceId && { stripeInvoiceId },
107
114
  ...stripeEventId && { stripeEventId }
108
- }
115
+ },
116
+ ...session && { options: { session } }
109
117
  });
110
118
  };
111
- var InsufficientCreditsError = class extends Error {
112
- constructor(message = "Insufficient AI credits") {
113
- super(message);
114
- this.name = "InsufficientCreditsError";
115
- this.status = 402;
116
- }
117
- };
118
119
  var debit = async ({
119
120
  db,
120
- authenticated,
121
+ user,
121
122
  userId,
122
123
  amount,
124
+ type,
123
125
  category,
124
- tags = [],
125
- provider,
126
+ source,
126
127
  stripeInvoiceId,
127
- stripeEventId
128
+ stripeEventId,
129
+ session,
130
+ ...rest
128
131
  }) => {
129
- var _a, _b, _c;
130
132
  if (!Number.isInteger(amount) || amount <= 0) {
131
133
  throw new Error(`debit() requires a positive integer amount, got ${amount}`);
132
134
  }
133
- const result = await db.update({
134
- authenticated,
135
- collection: "users",
136
- query: {
137
- id: userId,
138
- "balance.ai": {
139
- $gte: amount
140
- }
141
- },
142
- data: {
143
- $inc: {
144
- "balance.ai": -amount
145
- }
146
- }
147
- });
148
- 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);
149
- if (typeof after !== "number") {
150
- throw new InsufficientCreditsError();
135
+ if (!type) {
136
+ throw new Error('debit() requires a type (e.g., "ai")');
137
+ }
138
+ if (!source) {
139
+ throw new Error('debit() requires a source ("system" | "admin" | "user")');
151
140
  }
152
- const before = after + amount;
153
- await recordTransaction({
141
+ await insertTransaction({
154
142
  db,
155
- authenticated,
143
+ user,
156
144
  userId,
145
+ type,
157
146
  category,
158
- tags,
147
+ source,
159
148
  amount: -amount,
160
- balance: {
161
- before,
162
- after
163
- },
164
- provider,
165
149
  stripeInvoiceId,
166
- stripeEventId
150
+ stripeEventId,
151
+ session,
152
+ ...rest
167
153
  });
168
- return {
169
- amount: -amount,
170
- balance: {
171
- before,
172
- after
173
- }
174
- };
175
154
  };
176
155
 
177
156
  // ai.js
@@ -187,7 +166,7 @@ var models = {
187
166
  image: "gemini-2.5-flash-image",
188
167
  text: "gemini-2.5-flash"
189
168
  };
190
- var pricing = {
169
+ var cost = {
191
170
  "gemini-2.5-flash": {
192
171
  cached: 3,
193
172
  input: 30,
@@ -199,18 +178,33 @@ var pricing = {
199
178
  output: 250
200
179
  }
201
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
+ );
202
195
  var priceForRequest = (model, usage) => {
203
196
  const rates = pricing[model];
204
197
  if (!rates) {
205
198
  throw new Error(`Unknown model for pricing: ${model}`);
206
199
  }
207
- const tokens = {
208
- cached: Number(usage == null ? void 0 : usage.cached) || 0,
209
- input: Number(usage == null ? void 0 : usage.input) || 0,
210
- output: Number(usage == null ? void 0 : usage.output) || 0
211
- };
212
- const sum = tokens.cached * rates.cached + tokens.input * rates.input + tokens.output * rates.output;
213
- 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);
214
208
  };
215
209
  var tokensFromMetadata = (metadata) => ({
216
210
  cached: Number(metadata == null ? void 0 : metadata.cachedContentTokenCount) || 0,
@@ -224,19 +218,29 @@ var billRequest = async ({
224
218
  model,
225
219
  usage
226
220
  }) => {
227
- const amount = priceForRequest(model, usage);
221
+ const gross = priceForRequest(model, usage);
222
+ const cogs = costForRequest(model, usage);
228
223
  await debit({
229
224
  db,
230
225
  authenticated,
231
226
  userId,
232
- amount,
227
+ amount: gross,
228
+ type: "ai",
233
229
  category: "usage",
234
- tags: ["google", model],
235
- provider: {
230
+ source: "user",
231
+ ai: {
236
232
  name: "google",
237
233
  model,
238
234
  tokens: usage,
239
- 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
+ }
240
244
  }
241
245
  });
242
246
  };
@@ -322,6 +326,9 @@ var google = {
322
326
  };
323
327
  // Annotate the CommonJS export names for ESM import in node:
324
328
  0 && (module.exports = {
329
+ MARKUP,
330
+ cost,
331
+ costForRequest,
325
332
  google,
326
333
  priceForRequest,
327
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-RC5E56UB.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