@drawbridge/drawbridge-utils 0.0.26 → 0.0.28
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 +328 -0
- package/dist/ai.d.cts +244 -0
- package/dist/ai.d.ts +244 -0
- package/dist/ai.js +159 -0
- package/dist/chunk-6HH36OK6.js +54 -0
- package/dist/chunk-RC5E56UB.js +154 -0
- package/dist/circuit.js +3 -50
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +39 -1
- package/dist/index.d.ts +39 -1
- package/dist/index.js +20 -0
- package/dist/transactions.cjs +179 -0
- package/dist/transactions.d.cts +208 -0
- package/dist/transactions.d.ts +208 -0
- package/dist/transactions.js +10 -0
- package/package.json +13 -2
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import { circuit } from './circuit.js';
|
|
3
|
+
import { debit } from './transactions.js';
|
|
4
|
+
|
|
5
|
+
// AI request entry point. Wraps the Google GenAI client with a per-process
|
|
6
|
+
// circuit breaker, owns the per-model pricing table, and atomically debits
|
|
7
|
+
// the user's balance.ai based on the token usage returned by the provider.
|
|
8
|
+
// Both drawbridge-api routes and drawbridge-sync workers use this so the
|
|
9
|
+
// wrapper, pricing, and accounting all stay in one place.
|
|
10
|
+
//
|
|
11
|
+
// import { google } from '@drawbridge/drawbridge-utils/ai';
|
|
12
|
+
//
|
|
13
|
+
// const { content } = await google.text(
|
|
14
|
+
// { prompt : [ 'Write me a haiku' ] },
|
|
15
|
+
// { db, authenticated, userId }
|
|
16
|
+
// );
|
|
17
|
+
//
|
|
18
|
+
// If the user runs out of credits between the gating middleware and the
|
|
19
|
+
// debit call (a real but rare race), `debit` throws InsufficientCreditsError
|
|
20
|
+
// — the Google call has already happened, accepted trade-off vs. doing a
|
|
21
|
+
// second pre-flight balance read.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const google_client = new GoogleGenAI({
|
|
25
|
+
apiKey : process.env.GOOGLE_API_KEY
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const googleCircuit = circuit({
|
|
29
|
+
name : 'google-ai',
|
|
30
|
+
threshold : 3,
|
|
31
|
+
timeout : 60000
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const models = {
|
|
35
|
+
image : 'gemini-2.5-flash-image',
|
|
36
|
+
text : 'gemini-2.5-flash'
|
|
37
|
+
};
|
|
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
|
|
44
|
+
// https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json
|
|
45
|
+
// before any change.
|
|
46
|
+
const pricing = {
|
|
47
|
+
'gemini-2.5-flash' : {
|
|
48
|
+
cached : 3,
|
|
49
|
+
input : 30,
|
|
50
|
+
output : 250
|
|
51
|
+
},
|
|
52
|
+
'gemini-2.5-flash-image' : {
|
|
53
|
+
cached : 3,
|
|
54
|
+
input : 30,
|
|
55
|
+
output : 250
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
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.
|
|
62
|
+
const priceForRequest = ( model, usage ) => {
|
|
63
|
+
|
|
64
|
+
const rates = pricing[ model ];
|
|
65
|
+
|
|
66
|
+
if( !rates ){
|
|
67
|
+
|
|
68
|
+
throw new Error( `Unknown model for pricing: ${ model }` );
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tokens = {
|
|
73
|
+
cached : Number( usage?.cached ) || 0,
|
|
74
|
+
input : Number( usage?.input ) || 0,
|
|
75
|
+
output : Number( usage?.output ) || 0
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const sum = (
|
|
79
|
+
tokens.cached * rates.cached +
|
|
80
|
+
tokens.input * rates.input +
|
|
81
|
+
tokens.output * rates.output
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return Math.ceil( sum / 1_000_000 );
|
|
85
|
+
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Map Google's usageMetadata shape into the pricing.tokens shape.
|
|
89
|
+
const tokensFromMetadata = ( metadata ) => ({
|
|
90
|
+
cached : Number( metadata?.cachedContentTokenCount ) || 0,
|
|
91
|
+
input : Number( metadata?.promptTokenCount ) || 0,
|
|
92
|
+
output : Number( metadata?.candidatesTokenCount ) || 0
|
|
93
|
+
});
|
|
94
|
+
|
|
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.
|
|
97
|
+
const billRequest = async ({
|
|
98
|
+
db,
|
|
99
|
+
authenticated,
|
|
100
|
+
userId,
|
|
101
|
+
model,
|
|
102
|
+
usage
|
|
103
|
+
}) => {
|
|
104
|
+
|
|
105
|
+
const amount = priceForRequest( model, usage );
|
|
106
|
+
|
|
107
|
+
await debit({
|
|
108
|
+
db,
|
|
109
|
+
authenticated,
|
|
110
|
+
userId,
|
|
111
|
+
amount,
|
|
112
|
+
category : 'usage',
|
|
113
|
+
tags : [ 'google', model ],
|
|
114
|
+
provider : {
|
|
115
|
+
name : 'google',
|
|
116
|
+
model,
|
|
117
|
+
tokens : usage,
|
|
118
|
+
snapshot : pricing[ model ]
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const google = {
|
|
125
|
+
|
|
126
|
+
image : async (
|
|
127
|
+
{
|
|
128
|
+
config = {},
|
|
129
|
+
prompt = [],
|
|
130
|
+
reference = null
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
db,
|
|
134
|
+
authenticated,
|
|
135
|
+
userId
|
|
136
|
+
}
|
|
137
|
+
) => {
|
|
138
|
+
|
|
139
|
+
const model = models.image;
|
|
140
|
+
|
|
141
|
+
const parts = [
|
|
142
|
+
...( reference ? [ { inlineData : { data : reference.data, mimeType : reference.mimeType } } ] : [] ),
|
|
143
|
+
{ text : prompt.join( '\n' ) }
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
|
|
148
|
+
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
149
|
+
model,
|
|
150
|
+
contents : { parts },
|
|
151
|
+
config : {
|
|
152
|
+
...config,
|
|
153
|
+
responseModalities : [ 'Image' ]
|
|
154
|
+
}
|
|
155
|
+
}) );
|
|
156
|
+
|
|
157
|
+
const content = response?.candidates?.[ 0 ]?.content?.parts?.[ 0 ]?.inlineData?.data;
|
|
158
|
+
|
|
159
|
+
if( !content ){
|
|
160
|
+
|
|
161
|
+
throw new Error( 'Could not generate from prompt. Please try again with a different prompt.' );
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if( userId ){
|
|
166
|
+
|
|
167
|
+
await billRequest({
|
|
168
|
+
db,
|
|
169
|
+
authenticated,
|
|
170
|
+
userId,
|
|
171
|
+
model,
|
|
172
|
+
usage : tokensFromMetadata( response?.usageMetadata )
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { content };
|
|
178
|
+
|
|
179
|
+
} catch ( error ) {
|
|
180
|
+
|
|
181
|
+
throw new Error( error?.message || 'AI error, please try again' );
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
text : async (
|
|
188
|
+
{
|
|
189
|
+
config = {},
|
|
190
|
+
prompt = []
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
db,
|
|
194
|
+
authenticated,
|
|
195
|
+
userId
|
|
196
|
+
}
|
|
197
|
+
) => {
|
|
198
|
+
|
|
199
|
+
const model = models.text;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
|
|
203
|
+
const response = await googleCircuit( () => google_client.models.generateContent({
|
|
204
|
+
config : {
|
|
205
|
+
...config,
|
|
206
|
+
responseModalities : [ 'Text' ]
|
|
207
|
+
},
|
|
208
|
+
contents : prompt.join( '\n' ),
|
|
209
|
+
model
|
|
210
|
+
}) );
|
|
211
|
+
|
|
212
|
+
const content = response?.text;
|
|
213
|
+
|
|
214
|
+
if( !content ){
|
|
215
|
+
|
|
216
|
+
throw new Error( 'Could not generate from prompt. Please try again with a different prompt.' );
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if( userId ){
|
|
221
|
+
|
|
222
|
+
await billRequest({
|
|
223
|
+
db,
|
|
224
|
+
authenticated,
|
|
225
|
+
userId,
|
|
226
|
+
model,
|
|
227
|
+
usage : tokensFromMetadata( response?.usageMetadata )
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { content };
|
|
233
|
+
|
|
234
|
+
} catch ( error ) {
|
|
235
|
+
|
|
236
|
+
throw new Error( error?.message || 'AI error, please try again' );
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export { google, priceForRequest, pricing };
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debit
|
|
3
|
+
} from "./chunk-RC5E56UB.js";
|
|
4
|
+
import {
|
|
5
|
+
circuit
|
|
6
|
+
} from "./chunk-6HH36OK6.js";
|
|
7
|
+
|
|
8
|
+
// ai.js
|
|
9
|
+
import { GoogleGenAI } from "@google/genai";
|
|
10
|
+
var google_client = new GoogleGenAI({
|
|
11
|
+
apiKey: process.env.GOOGLE_API_KEY
|
|
12
|
+
});
|
|
13
|
+
var googleCircuit = circuit({
|
|
14
|
+
name: "google-ai",
|
|
15
|
+
threshold: 3,
|
|
16
|
+
timeout: 6e4
|
|
17
|
+
});
|
|
18
|
+
var models = {
|
|
19
|
+
image: "gemini-2.5-flash-image",
|
|
20
|
+
text: "gemini-2.5-flash"
|
|
21
|
+
};
|
|
22
|
+
var pricing = {
|
|
23
|
+
"gemini-2.5-flash": {
|
|
24
|
+
cached: 3,
|
|
25
|
+
input: 30,
|
|
26
|
+
output: 250
|
|
27
|
+
},
|
|
28
|
+
"gemini-2.5-flash-image": {
|
|
29
|
+
cached: 3,
|
|
30
|
+
input: 30,
|
|
31
|
+
output: 250
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var priceForRequest = (model, usage) => {
|
|
35
|
+
const rates = pricing[model];
|
|
36
|
+
if (!rates) {
|
|
37
|
+
throw new Error(`Unknown model for pricing: ${model}`);
|
|
38
|
+
}
|
|
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);
|
|
46
|
+
};
|
|
47
|
+
var tokensFromMetadata = (metadata) => ({
|
|
48
|
+
cached: Number(metadata == null ? void 0 : metadata.cachedContentTokenCount) || 0,
|
|
49
|
+
input: Number(metadata == null ? void 0 : metadata.promptTokenCount) || 0,
|
|
50
|
+
output: Number(metadata == null ? void 0 : metadata.candidatesTokenCount) || 0
|
|
51
|
+
});
|
|
52
|
+
var billRequest = async ({
|
|
53
|
+
db,
|
|
54
|
+
authenticated,
|
|
55
|
+
userId,
|
|
56
|
+
model,
|
|
57
|
+
usage
|
|
58
|
+
}) => {
|
|
59
|
+
const amount = priceForRequest(model, usage);
|
|
60
|
+
await debit({
|
|
61
|
+
db,
|
|
62
|
+
authenticated,
|
|
63
|
+
userId,
|
|
64
|
+
amount,
|
|
65
|
+
category: "usage",
|
|
66
|
+
tags: ["google", model],
|
|
67
|
+
provider: {
|
|
68
|
+
name: "google",
|
|
69
|
+
model,
|
|
70
|
+
tokens: usage,
|
|
71
|
+
snapshot: pricing[model]
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
var google = {
|
|
76
|
+
image: async ({
|
|
77
|
+
config = {},
|
|
78
|
+
prompt = [],
|
|
79
|
+
reference = null
|
|
80
|
+
}, {
|
|
81
|
+
db,
|
|
82
|
+
authenticated,
|
|
83
|
+
userId
|
|
84
|
+
}) => {
|
|
85
|
+
var _a, _b, _c, _d, _e, _f;
|
|
86
|
+
const model = models.image;
|
|
87
|
+
const parts = [
|
|
88
|
+
...reference ? [{ inlineData: { data: reference.data, mimeType: reference.mimeType } }] : [],
|
|
89
|
+
{ text: prompt.join("\n") }
|
|
90
|
+
];
|
|
91
|
+
try {
|
|
92
|
+
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
93
|
+
model,
|
|
94
|
+
contents: { parts },
|
|
95
|
+
config: {
|
|
96
|
+
...config,
|
|
97
|
+
responseModalities: ["Image"]
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
const content = (_f = (_e = (_d = (_c = (_b = (_a = response == null ? void 0 : response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d[0]) == null ? void 0 : _e.inlineData) == null ? void 0 : _f.data;
|
|
101
|
+
if (!content) {
|
|
102
|
+
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
|
103
|
+
}
|
|
104
|
+
if (userId) {
|
|
105
|
+
await billRequest({
|
|
106
|
+
db,
|
|
107
|
+
authenticated,
|
|
108
|
+
userId,
|
|
109
|
+
model,
|
|
110
|
+
usage: tokensFromMetadata(response == null ? void 0 : response.usageMetadata)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { content };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error((error == null ? void 0 : error.message) || "AI error, please try again");
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
text: async ({
|
|
119
|
+
config = {},
|
|
120
|
+
prompt = []
|
|
121
|
+
}, {
|
|
122
|
+
db,
|
|
123
|
+
authenticated,
|
|
124
|
+
userId
|
|
125
|
+
}) => {
|
|
126
|
+
const model = models.text;
|
|
127
|
+
try {
|
|
128
|
+
const response = await googleCircuit(() => google_client.models.generateContent({
|
|
129
|
+
config: {
|
|
130
|
+
...config,
|
|
131
|
+
responseModalities: ["Text"]
|
|
132
|
+
},
|
|
133
|
+
contents: prompt.join("\n"),
|
|
134
|
+
model
|
|
135
|
+
}));
|
|
136
|
+
const content = response == null ? void 0 : response.text;
|
|
137
|
+
if (!content) {
|
|
138
|
+
throw new Error("Could not generate from prompt. Please try again with a different prompt.");
|
|
139
|
+
}
|
|
140
|
+
if (userId) {
|
|
141
|
+
await billRequest({
|
|
142
|
+
db,
|
|
143
|
+
authenticated,
|
|
144
|
+
userId,
|
|
145
|
+
model,
|
|
146
|
+
usage: tokensFromMetadata(response == null ? void 0 : response.usageMetadata)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return { content };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error((error == null ? void 0 : error.message) || "AI error, please try again");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
export {
|
|
156
|
+
google,
|
|
157
|
+
priceForRequest,
|
|
158
|
+
pricing
|
|
159
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// circuit.js
|
|
2
|
+
var CLOSED = "CLOSED";
|
|
3
|
+
var OPEN = "OPEN";
|
|
4
|
+
var HALF_OPEN = "HALF_OPEN";
|
|
5
|
+
var circuit = ({
|
|
6
|
+
name,
|
|
7
|
+
threshold = 5,
|
|
8
|
+
timeout = 3e4
|
|
9
|
+
}) => {
|
|
10
|
+
let state = CLOSED;
|
|
11
|
+
let failures = 0;
|
|
12
|
+
let openedAt = null;
|
|
13
|
+
const trip = () => {
|
|
14
|
+
state = OPEN;
|
|
15
|
+
openedAt = Date.now();
|
|
16
|
+
console.error(`Circuit breaker OPEN: ${name}`);
|
|
17
|
+
};
|
|
18
|
+
const reset = () => {
|
|
19
|
+
state = CLOSED;
|
|
20
|
+
failures = 0;
|
|
21
|
+
openedAt = null;
|
|
22
|
+
console.log(`Circuit breaker CLOSED: ${name}`);
|
|
23
|
+
};
|
|
24
|
+
return async (fn) => {
|
|
25
|
+
if (state === OPEN) {
|
|
26
|
+
if (Date.now() - openedAt >= timeout) {
|
|
27
|
+
state = HALF_OPEN;
|
|
28
|
+
} else {
|
|
29
|
+
const error = new Error(`${name} is temporarily unavailable`);
|
|
30
|
+
error.status = 503;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = await fn();
|
|
36
|
+
if (state === HALF_OPEN) {
|
|
37
|
+
reset();
|
|
38
|
+
} else {
|
|
39
|
+
failures = 0;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
failures++;
|
|
44
|
+
if (state === HALF_OPEN || failures >= threshold) {
|
|
45
|
+
trip();
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
circuit
|
|
54
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// transactions.js
|
|
2
|
+
var recordTransaction = async ({
|
|
3
|
+
db,
|
|
4
|
+
authenticated,
|
|
5
|
+
userId,
|
|
6
|
+
category,
|
|
7
|
+
tags,
|
|
8
|
+
amount,
|
|
9
|
+
balance,
|
|
10
|
+
provider,
|
|
11
|
+
stripeInvoiceId,
|
|
12
|
+
stripeEventId
|
|
13
|
+
}) => {
|
|
14
|
+
await db.create({
|
|
15
|
+
authenticated,
|
|
16
|
+
collection: "transactions",
|
|
17
|
+
data: {
|
|
18
|
+
userId,
|
|
19
|
+
billable: {
|
|
20
|
+
item: "ai"
|
|
21
|
+
},
|
|
22
|
+
category,
|
|
23
|
+
tags,
|
|
24
|
+
amount,
|
|
25
|
+
balance,
|
|
26
|
+
...provider && { provider },
|
|
27
|
+
...stripeInvoiceId && { stripeInvoiceId },
|
|
28
|
+
...stripeEventId && { stripeEventId }
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
var credit = async ({
|
|
33
|
+
db,
|
|
34
|
+
authenticated,
|
|
35
|
+
userId,
|
|
36
|
+
amount,
|
|
37
|
+
category,
|
|
38
|
+
tags = [],
|
|
39
|
+
stripeInvoiceId,
|
|
40
|
+
stripeEventId
|
|
41
|
+
}) => {
|
|
42
|
+
var _a, _b, _c;
|
|
43
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
44
|
+
throw new Error(`credit() requires a positive integer amount, got ${amount}`);
|
|
45
|
+
}
|
|
46
|
+
const result = await db.update({
|
|
47
|
+
authenticated,
|
|
48
|
+
collection: "users",
|
|
49
|
+
query: {
|
|
50
|
+
id: userId
|
|
51
|
+
},
|
|
52
|
+
data: {
|
|
53
|
+
$inc: {
|
|
54
|
+
"balance.ai": amount
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
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);
|
|
59
|
+
if (typeof after !== "number") {
|
|
60
|
+
throw new Error(`credit() could not read updated balance for user ${userId}`);
|
|
61
|
+
}
|
|
62
|
+
const before = after - amount;
|
|
63
|
+
await recordTransaction({
|
|
64
|
+
db,
|
|
65
|
+
authenticated,
|
|
66
|
+
userId,
|
|
67
|
+
category,
|
|
68
|
+
tags,
|
|
69
|
+
amount,
|
|
70
|
+
balance: {
|
|
71
|
+
before,
|
|
72
|
+
after
|
|
73
|
+
},
|
|
74
|
+
stripeInvoiceId,
|
|
75
|
+
stripeEventId
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
balance: {
|
|
79
|
+
before,
|
|
80
|
+
after
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var InsufficientCreditsError = class extends Error {
|
|
85
|
+
constructor(message = "Insufficient AI credits") {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = "InsufficientCreditsError";
|
|
88
|
+
this.status = 402;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var debit = async ({
|
|
92
|
+
db,
|
|
93
|
+
authenticated,
|
|
94
|
+
userId,
|
|
95
|
+
amount,
|
|
96
|
+
category,
|
|
97
|
+
tags = [],
|
|
98
|
+
provider,
|
|
99
|
+
stripeInvoiceId,
|
|
100
|
+
stripeEventId
|
|
101
|
+
}) => {
|
|
102
|
+
var _a, _b, _c;
|
|
103
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
104
|
+
throw new Error(`debit() requires a positive integer amount, got ${amount}`);
|
|
105
|
+
}
|
|
106
|
+
const result = await db.update({
|
|
107
|
+
authenticated,
|
|
108
|
+
collection: "users",
|
|
109
|
+
query: {
|
|
110
|
+
id: userId,
|
|
111
|
+
"balance.ai": {
|
|
112
|
+
$gte: amount
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
data: {
|
|
116
|
+
$inc: {
|
|
117
|
+
"balance.ai": -amount
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
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);
|
|
122
|
+
if (typeof after !== "number") {
|
|
123
|
+
throw new InsufficientCreditsError();
|
|
124
|
+
}
|
|
125
|
+
const before = after + amount;
|
|
126
|
+
await recordTransaction({
|
|
127
|
+
db,
|
|
128
|
+
authenticated,
|
|
129
|
+
userId,
|
|
130
|
+
category,
|
|
131
|
+
tags,
|
|
132
|
+
amount: -amount,
|
|
133
|
+
balance: {
|
|
134
|
+
before,
|
|
135
|
+
after
|
|
136
|
+
},
|
|
137
|
+
provider,
|
|
138
|
+
stripeInvoiceId,
|
|
139
|
+
stripeEventId
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
amount: -amount,
|
|
143
|
+
balance: {
|
|
144
|
+
before,
|
|
145
|
+
after
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
credit,
|
|
152
|
+
InsufficientCreditsError,
|
|
153
|
+
debit
|
|
154
|
+
};
|
package/dist/circuit.js
CHANGED
|
@@ -1,53 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var HALF_OPEN = "HALF_OPEN";
|
|
5
|
-
var circuit = ({
|
|
6
|
-
name,
|
|
7
|
-
threshold = 5,
|
|
8
|
-
timeout = 3e4
|
|
9
|
-
}) => {
|
|
10
|
-
let state = CLOSED;
|
|
11
|
-
let failures = 0;
|
|
12
|
-
let openedAt = null;
|
|
13
|
-
const trip = () => {
|
|
14
|
-
state = OPEN;
|
|
15
|
-
openedAt = Date.now();
|
|
16
|
-
console.error(`Circuit breaker OPEN: ${name}`);
|
|
17
|
-
};
|
|
18
|
-
const reset = () => {
|
|
19
|
-
state = CLOSED;
|
|
20
|
-
failures = 0;
|
|
21
|
-
openedAt = null;
|
|
22
|
-
console.log(`Circuit breaker CLOSED: ${name}`);
|
|
23
|
-
};
|
|
24
|
-
return async (fn) => {
|
|
25
|
-
if (state === OPEN) {
|
|
26
|
-
if (Date.now() - openedAt >= timeout) {
|
|
27
|
-
state = HALF_OPEN;
|
|
28
|
-
} else {
|
|
29
|
-
const error = new Error(`${name} is temporarily unavailable`);
|
|
30
|
-
error.status = 503;
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const result = await fn();
|
|
36
|
-
if (state === HALF_OPEN) {
|
|
37
|
-
reset();
|
|
38
|
-
} else {
|
|
39
|
-
failures = 0;
|
|
40
|
-
}
|
|
41
|
-
return result;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
failures++;
|
|
44
|
-
if (state === HALF_OPEN || failures >= threshold) {
|
|
45
|
-
trip();
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
circuit
|
|
3
|
+
} from "./chunk-6HH36OK6.js";
|
|
51
4
|
export {
|
|
52
5
|
circuit
|
|
53
6
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
formatNumber: () => formatNumber,
|
|
32
32
|
getPlanFeature: () => getPlanFeature,
|
|
33
33
|
gigabyte: () => gigabyte,
|
|
34
|
+
incrementUsageTotals: () => incrementUsageTotals,
|
|
34
35
|
infinite: () => infinite,
|
|
35
36
|
isInfinite: () => isInfinite,
|
|
36
37
|
megabyte: () => megabyte,
|
|
@@ -309,6 +310,25 @@ var getPlanFeature = (plan, key) => {
|
|
|
309
310
|
message: granted ? feature : error
|
|
310
311
|
};
|
|
311
312
|
};
|
|
313
|
+
var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
|
|
314
|
+
if (!usageId || !$inc) return;
|
|
315
|
+
const safeInc = Object.fromEntries(
|
|
316
|
+
Object.entries($inc).filter(([, value]) => {
|
|
317
|
+
const number = Number(value);
|
|
318
|
+
return Number.isFinite(number) && number !== 0;
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
if (Object.keys(safeInc).length === 0) return;
|
|
322
|
+
return controller.update({
|
|
323
|
+
collection: "usage",
|
|
324
|
+
data: { $inc: safeInc },
|
|
325
|
+
options: {
|
|
326
|
+
bypassDocumentValidation: true,
|
|
327
|
+
...session && { session }
|
|
328
|
+
},
|
|
329
|
+
query: { id: usageId }
|
|
330
|
+
});
|
|
331
|
+
};
|
|
312
332
|
var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
|
|
313
333
|
var reducers = {
|
|
314
334
|
fields: (data2 = [], callback = () => ({})) => data2.reduce(
|
|
@@ -400,6 +420,7 @@ var currency = (val) => (0, import_currency_codes.code)(val);
|
|
|
400
420
|
formatNumber,
|
|
401
421
|
getPlanFeature,
|
|
402
422
|
gigabyte,
|
|
423
|
+
incrementUsageTotals,
|
|
403
424
|
infinite,
|
|
404
425
|
isInfinite,
|
|
405
426
|
megabyte,
|